From 5fb0f9a1a717141548e1198d16ac36f70903a710 Mon Sep 17 00:00:00 2001 From: lq Date: Wed, 12 Aug 2026 13:52:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8D=87=E7=BA=A7=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E8=A7=86=E8=A7=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite-tailwindcss/package.json | 2 + vite-tailwindcss/pnpm-lock.yaml | 26 +- vite-tailwindcss/src/components/AppToast.vue | 8 +- .../src/components/BorderStylePicker.vue | 143 ++ .../src/components/DanmakuLayer.vue | 54 +- .../src/components/GiftEffect3D.vue | 1135 +++++++++++++++ .../src/components/ImageField.vue | 6 +- .../src/components/MediaPickerModal.vue | 18 +- .../src/components/PhotoGallery.vue | 294 +++- .../src/composables/borderStyles.js | 58 +- .../src/composables/heartCollage.js | 41 +- .../src/composables/posterDefaults.js | 46 +- .../src/composables/resizeImage.js | 12 + vite-tailwindcss/src/directives/reveal.js | 3 +- vite-tailwindcss/src/main.js | 1 + vite-tailwindcss/src/style.css | 288 ++++ vite-tailwindcss/src/styles/animations.css | 51 +- vite-tailwindcss/src/styles/frames.css | 580 ++++++++ vite-tailwindcss/src/utils/danmakuColors.js | 66 +- vite-tailwindcss/src/utils/themes.js | 125 ++ .../src/views/admin/AdminEditor.vue | 1261 +++++++++++++---- .../src/views/admin/AdminLogin.vue | 22 +- .../views/admin/HotelWelcomePosterAdmin.vue | 74 +- vite-tailwindcss/src/views/hb/index.vue | 202 ++- .../src/views/hb/templates/ClassicPoster.vue | 56 +- .../src/views/hb/templates/SplitPoster.vue | 52 +- vite-tailwindcss/src/views/index/index.vue | 1234 ++++++++++++---- 27 files changed, 5032 insertions(+), 826 deletions(-) create mode 100644 vite-tailwindcss/src/components/BorderStylePicker.vue create mode 100644 vite-tailwindcss/src/components/GiftEffect3D.vue create mode 100644 vite-tailwindcss/src/utils/themes.js diff --git a/vite-tailwindcss/package.json b/vite-tailwindcss/package.json index 8bcb065..33826c7 100644 --- a/vite-tailwindcss/package.json +++ b/vite-tailwindcss/package.json @@ -13,6 +13,7 @@ "@ant-design/icons-vue": "^7.0.1", "@fingerprintjs/fingerprintjs": "^5.2.0", "@fontsource/cinzel": "^5.3.0", + "@fontsource/fusion-pixel-12px-proportional-sc": "^5.3.0", "@fontsource/great-vibes": "^5.3.0", "@fontsource/liu-jian-mao-cao": "^5.3.0", "@fontsource/long-cang": "^5.3.0", @@ -34,6 +35,7 @@ "qrcode": "^1.5.4", "swiper": "^11.0.5", "tailwindcss": "^4.1.4", + "three": "^0.185.1", "vue": "^3.5.13", "vue-router": "^4.2.5" }, diff --git a/vite-tailwindcss/pnpm-lock.yaml b/vite-tailwindcss/pnpm-lock.yaml index 3b28d6a..6c99638 100644 --- a/vite-tailwindcss/pnpm-lock.yaml +++ b/vite-tailwindcss/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@fontsource/cinzel': specifier: ^5.3.0 version: 5.3.0 + '@fontsource/fusion-pixel-12px-proportional-sc': + specifier: ^5.3.0 + version: 5.3.0 '@fontsource/great-vibes': specifier: ^5.3.0 version: 5.3.0 @@ -55,7 +58,7 @@ importers: version: 4.2.6(vue@3.5.40) axios: specifier: ^1.6.2 - version: 1.18.1(debug@4.4.3) + version: 1.18.1 echarts: specifier: ^6.1.0 version: 6.1.0 @@ -83,6 +86,9 @@ importers: tailwindcss: specifier: ^4.1.4 version: 4.3.3 + three: + specifier: ^0.185.1 + version: 0.185.1 vue: specifier: ^3.5.13 version: 3.5.40 @@ -317,6 +323,9 @@ packages: '@fontsource/cinzel@5.3.0': resolution: {integrity: sha512-KEOYTsrRppW0uRIPWBDPrQgunCS6f8u4nzZxYRBfo9SCknKojLYjK7B0ZoVmJhyVLefz271ZQRF9EOpHZlfhRw==} + '@fontsource/fusion-pixel-12px-proportional-sc@5.3.0': + resolution: {integrity: sha512-PDgfCunZbVgPM5/HSaP29KjQaxLUsVAng6urpSQXww87e9AOfA7j/PDzWYO/Q8yhPyaw6oUGh5s2eGln1HSgPw==} + '@fontsource/great-vibes@5.3.0': resolution: {integrity: sha512-1w60cXzXnC9V4y943po8044O1mqRQBiTTK75h7OpZE+1VQtx+KlAAzoIyEE5KNzu05gUpxaCQg7cbL7ITSwegQ==} @@ -1274,6 +1283,9 @@ packages: text-segmentation@1.0.3: resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -1521,6 +1533,8 @@ snapshots: '@fontsource/cinzel@5.3.0': {} + '@fontsource/fusion-pixel-12px-proportional-sc@5.3.0': {} + '@fontsource/great-vibes@5.3.0': {} '@fontsource/liu-jian-mao-cao@5.3.0': {} @@ -1919,9 +1933,9 @@ snapshots: asynckit@0.4.0: {} - axios@1.18.1(debug@4.4.3): + axios@1.18.1: dependencies: - follow-redirects: 1.16.0(debug@4.4.3) + follow-redirects: 1.16.0 form-data: 4.0.6 https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 @@ -2061,9 +2075,7 @@ snapshots: locate-path: 5.0.0 path-exists: 4.0.0 - follow-redirects@1.16.0(debug@4.4.3): - optionalDependencies: - debug: 4.4.3 + follow-redirects@1.16.0: {} form-data@4.0.6: dependencies: @@ -2382,6 +2394,8 @@ snapshots: dependencies: utrie: 1.0.2 + three@0.185.1: {} + throttle-debounce@5.0.2: {} tinyglobby@0.2.17: diff --git a/vite-tailwindcss/src/components/AppToast.vue b/vite-tailwindcss/src/components/AppToast.vue index d45cf98..6981aa9 100644 --- a/vite-tailwindcss/src/components/AppToast.vue +++ b/vite-tailwindcss/src/components/AppToast.vue @@ -51,13 +51,13 @@ const { toasts, dismiss } = useToast() padding: 12px 14px; border-radius: 16px; background: rgba(253, 251, 247, 0.96); - border: 0.5px solid rgba(168, 140, 107, 0.35); + border: 0.5px solid rgba(var(--c-primary-rgb), 0.35); box-shadow: 0 12px 32px rgba(92, 74, 53, 0.16); backdrop-filter: blur(8px); color: #5c4a35; } .app-toast.is-success { - border-color: rgba(168, 140, 107, 0.5); + border-color: rgba(var(--c-primary-rgb), 0.5); } .app-toast.is-error { border-color: rgba(196, 120, 110, 0.55); @@ -73,8 +73,8 @@ const { toasts, dismiss } = useToast() justify-content: center; margin-top: 1px; font-size: 11px; - background: rgba(168, 140, 107, 0.14); - color: #a88c6b; + background: rgba(var(--c-primary-rgb), 0.14); + color: var(--c-primary); } .app-toast.is-error .app-toast-icon { background: rgba(196, 120, 110, 0.16); diff --git a/vite-tailwindcss/src/components/BorderStylePicker.vue b/vite-tailwindcss/src/components/BorderStylePicker.vue new file mode 100644 index 0000000..44d2098 --- /dev/null +++ b/vite-tailwindcss/src/components/BorderStylePicker.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/vite-tailwindcss/src/components/DanmakuLayer.vue b/vite-tailwindcss/src/components/DanmakuLayer.vue index f884a60..da979b8 100644 --- a/vite-tailwindcss/src/components/DanmakuLayer.vue +++ b/vite-tailwindcss/src/components/DanmakuLayer.vue @@ -10,25 +10,22 @@ v-for="row in rows" :key="row.id" class="danmaku-item" - :class="{ - 'danmaku-item--grad': row.isGradient, - 'danmaku-item--grad-light': row.isGradient && row.textLight, - 'danmaku-item--grad-dark': row.isGradient && !row.textLight, - }" + :class="{ 'danmaku-item--grad': row.isGradient }" :style="row.style" @animationend="onEnded(row.id)" > - {{ row.name }} - - {{ row.content }} + {{ row.timeLabel }} + v-if="row.textGradient" + class="danmaku-body danmaku-body--grad" + :style="{ backgroundImage: row.textGradient }" + >{{ row.name }}{{ row.content }} + + {{ row.timeLabel }} @@ -173,7 +170,6 @@ const buildRow = (item, { force = false } = {}) => { const totalDelay = delay + slot.extraDelay // 轨道分布在当前弹幕层内部(由 area 控制层高度) const topPct = TRACKS <= 1 ? 50 : 12 + (slot.track / (TRACKS - 1)) * 70 - const isGradient = typeof tint.background === 'string' && tint.background.includes('gradient') const timeLabel = force ? '刚刚' @@ -184,8 +180,8 @@ const buildRow = (item, { force = false } = {}) => { name: item.name, content: item.content, color: resolveDanmakuColor(item.color), - textLight: !!tint.textLight, - isGradient, + isGradient: !!tint.textGradient, + textGradient: tint.textGradient || '', timeLabel, style: { top: `${topPct}%`, @@ -377,7 +373,7 @@ defineExpose({ refresh: fetchList, pushItem }) white-space: nowrap; padding: 5px 14px; border-radius: 999px; - border: 1px solid rgba(168, 140, 107, 0.28); + border: 1px solid rgba(var(--c-primary-rgb), 0.28); box-shadow: 0 4px 14px rgba(88, 68, 42, 0.08); font-size: 12px; letter-spacing: 0.06em; @@ -389,14 +385,18 @@ defineExpose({ refresh: fetchList, pushItem }) max-width: none; } .danmaku-item--grad { - backdrop-filter: saturate(1.2) blur(8px); + backdrop-filter: saturate(1.15) blur(7px); font-weight: 500; } -.danmaku-item--grad-light { - text-shadow: 0 1px 2px rgba(20, 10, 20, 0.45), 0 0 1px rgba(0, 0, 0, 0.2); +/* 渐变字:颜色裁切进文字,底子保持通透 */ +.danmaku-body--grad { + -webkit-background-clip: text; + background-clip: text; + color: transparent; } -.danmaku-item--grad-dark { - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35); +.danmaku-body--grad .danmaku-sep, +.danmaku-body--grad .danmaku-text { + opacity: 1; } .danmaku-name { font-weight: 600; } .danmaku-sep { opacity: 0.9; } @@ -407,12 +407,6 @@ defineExpose({ refresh: fetchList, pushItem }) font-size: 10px; letter-spacing: 0.04em; } -.danmaku-time--on-light { - color: rgba(255, 255, 255, 0.88); -} -.danmaku-time--on-dark { - color: rgba(74, 52, 32, 0.72); -} @keyframes danmaku-fly { from { transform: translateX(0); } to { transform: translateX(calc(-100vw - 100%)); } diff --git a/vite-tailwindcss/src/components/GiftEffect3D.vue b/vite-tailwindcss/src/components/GiftEffect3D.vue new file mode 100644 index 0000000..9901b71 --- /dev/null +++ b/vite-tailwindcss/src/components/GiftEffect3D.vue @@ -0,0 +1,1135 @@ + + + + + diff --git a/vite-tailwindcss/src/components/ImageField.vue b/vite-tailwindcss/src/components/ImageField.vue index 0f41057..0ddabf6 100644 --- a/vite-tailwindcss/src/components/ImageField.vue +++ b/vite-tailwindcss/src/components/ImageField.vue @@ -1,17 +1,17 @@ diff --git a/vite-tailwindcss/src/components/MediaPickerModal.vue b/vite-tailwindcss/src/components/MediaPickerModal.vue index b8ebacd..1edd9b9 100644 --- a/vite-tailwindcss/src/components/MediaPickerModal.vue +++ b/vite-tailwindcss/src/components/MediaPickerModal.vue @@ -8,7 +8,7 @@ @cancel="$emit('update:open', false)" >
- + 上传新图 @@ -153,12 +153,12 @@ watch( } .mp-hint { font-size: 12px; - color: #8a8680; + color: var(--c-muted); margin-left: auto; } .mp-empty { text-align: center; - color: #8a8680; + color: var(--c-muted); padding: 48px 12px; font-size: 13px; } @@ -170,8 +170,8 @@ watch( } .mp-item { appearance: none; - border: 1px solid rgba(168, 140, 107, 0.28); - background: #fdfbf7; + border: 1px solid rgba(var(--c-primary-rgb), 0.28); + background: var(--c-paper); border-radius: 10px; padding: 0; overflow: hidden; @@ -181,21 +181,21 @@ watch( } .mp-item:hover, .mp-item.active { - border-color: #a88c6b; - box-shadow: 0 0 0 2px rgba(168, 140, 107, 0.2); + border-color: var(--c-primary); + box-shadow: 0 0 0 2px rgba(var(--c-primary-rgb), 0.2); } .mp-item img { display: block; width: 100%; height: 88px; object-fit: cover; - background: #f0ebe3; + background: var(--c-bg-deep); } .mp-name { display: block; padding: 4px 6px; font-size: 10px; - color: #8a8680; + color: var(--c-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/vite-tailwindcss/src/components/PhotoGallery.vue b/vite-tailwindcss/src/components/PhotoGallery.vue index 8182f59..b0d5e42 100644 --- a/vite-tailwindcss/src/components/PhotoGallery.vue +++ b/vite-tailwindcss/src/components/PhotoGallery.vue @@ -1,17 +1,26 @@ diff --git a/vite-tailwindcss/src/composables/borderStyles.js b/vite-tailwindcss/src/composables/borderStyles.js index 4675d78..c8b7e3c 100644 --- a/vite-tailwindcss/src/composables/borderStyles.js +++ b/vite-tailwindcss/src/composables/borderStyles.js @@ -1,18 +1,54 @@ // 相册边框样式定义:供前台 PhotoGallery 渲染与后台 AdminEditor 配置/预览共用 +// - group : base=常规矩形边框;postcard=明信片·异形(mask/clip 裁切类) +// - shaped: mask/clip 裁切类(box-shadow 会随之被裁掉,需要父级加 .gf-shape-shadow 用 drop-shadow 补投影) +// - ratio : 单图版式下推荐的宽高比(未设置时默认 4 / 5) export const BORDER_STYLES = [ - { value: 'none', label: '无边框' }, - { value: 'thin', label: '细金线' }, - { value: 'mat', label: '白卡纸' }, - { value: 'rounded', label: '柔圆角' }, - { value: 'double', label: '双线框' }, - { value: 'gold', label: '古典金框' }, - { value: 'shadow', label: '阴影浮起' }, - { value: 'ink', label: '墨黑描边' }, - { value: 'card', label: '悬浮卡片' }, - { value: 'lace', label: '虚线文艺' }, - { value: 'glow', label: '柔光金晕' }, + { value: 'none', label: '无边框', group: 'base' }, + { value: 'thin', label: '细金线', group: 'base' }, + { value: 'mat', label: '白卡纸', group: 'base' }, + { value: 'rounded', label: '柔圆角', group: 'base' }, + { value: 'double', label: '双线框', group: 'base' }, + { value: 'gold', label: '古典金框', group: 'base' }, + { value: 'shadow', label: '阴影浮起', group: 'base' }, + { value: 'ink', label: '墨黑描边', group: 'base' }, + { value: 'card', label: '悬浮卡片', group: 'base' }, + { value: 'lace', label: '虚线文艺', group: 'base' }, + { value: 'glow', label: '柔光金晕', group: 'base' }, + { value: 'stamp', label: '邮票齿孔', group: 'postcard', shaped: true }, + { value: 'postcard', label: '复古明信片', group: 'postcard' }, + { value: 'arch', label: '拱门', group: 'postcard', ratio: '3 / 4' }, + { value: 'oval', label: '椭圆相框', group: 'postcard', ratio: '3 / 4' }, + { value: 'circle', label: '正圆相框', group: 'postcard', ratio: '1 / 1' }, + { value: 'heart', label: '心形', group: 'postcard', shaped: true, ratio: '1 / 1' }, + { value: 'flower', label: '八瓣花', group: 'postcard', shaped: true, ratio: '1 / 1' }, + { value: 'leaf', label: '叶形花瓣', group: 'postcard', ratio: '1 / 1' }, + { value: 'hexagon', label: '六边形', group: 'postcard', shaped: true, ratio: '1 / 1' }, + { value: 'scallop', label: '扇贝花边', group: 'postcard', shaped: true }, + { value: 'wave', label: '蕾丝波浪', group: 'postcard', shaped: true }, + { value: 'zigzag', label: '锯齿花边', group: 'postcard', shaped: true }, + { value: 'torn', label: '撕纸边', group: 'postcard', shaped: true }, + { value: 'bevel', label: '切角相纸', group: 'postcard', shaped: true }, + { value: 'ticket', label: '车票票根', group: 'postcard', shaped: true }, + { value: 'film', label: '复古胶片', group: 'postcard' }, + { value: 'cloud', label: '云朵相框', group: 'scenic', shaped: true, ratio: '4 / 3' }, + { value: 'mist', label: '晨雾朦胧', group: 'scenic', ratio: '4 / 3' }, + { value: 'bloom', label: '花语卡纸', group: 'scenic', ratio: '4 / 3' }, + { value: 'panorama', label: '宽幅全景', group: 'scenic', ratio: '16 / 9' }, + { value: 'ovalwide', label: '横椭圆框', group: 'scenic', ratio: '4 / 3' }, ] +// 供可视化选择器分组展示 +export const BORDER_STYLE_GROUPS = [ + { key: 'base', label: '基础边框', items: BORDER_STYLES.filter((b) => b.group === 'base') }, + { key: 'postcard', label: '明信片 · 异形', items: BORDER_STYLES.filter((b) => b.group === 'postcard') }, + { key: 'scenic', label: '横幅 · 云雾花', items: BORDER_STYLES.filter((b) => b.group === 'scenic') }, +] + +/** 取样式元数据(未知值回落白卡纸) */ +export function borderStyleMeta(style) { + return BORDER_STYLES.find((b) => b.value === style) || BORDER_STYLES.find((b) => b.value === 'mat') +} + export function borderClass(style) { const v = BORDER_STYLES.some((b) => b.value === style) ? style : 'mat' return 'gf-frame gf-' + v diff --git a/vite-tailwindcss/src/composables/heartCollage.js b/vite-tailwindcss/src/composables/heartCollage.js index 59d39fa..e5d8701 100644 --- a/vite-tailwindcss/src/composables/heartCollage.js +++ b/vite-tailwindcss/src/composables/heartCollage.js @@ -1,20 +1,23 @@ import { clampHeroFocus, heroFocusCss } from '@/composables/posterDefaults' -/** 爱心拼接槽位:百分比绝对定位(顶开叉、中最宽、底收尖) */ +/** 爱心拼接槽位:百分比绝对定位(顶开叉、中最宽、底收尖) + * r : 格子圆角(外缘格用大弧度圆角把方块阶梯"磨"成心形弧线) + * clip : 格子裁切(心尖用 V 形三角收出尖端) + * 槽位索引与角色保持不变,存量配置的图片顺序不受影响 */ export const HEART_SLOTS = [ - { l: 10, t: 0, w: 32, h: 13 }, // 0 左上叶 - { l: 58, t: 0, w: 32, h: 13 }, // 1 右上叶 - { l: 0, t: 14, w: 14, h: 15.5 }, // 2 左外上 - { l: 0, t: 30.5, w: 14, h: 15.5 }, // 3 左外下 - { l: 15, t: 14, w: 26, h: 32 }, // 4 左大图 - { l: 42, t: 14, w: 16, h: 32 }, // 5 中轴 - { l: 59, t: 14, w: 26, h: 32 }, // 6 右大图 - { l: 86, t: 14, w: 14, h: 15.5 }, // 7 右外上 - { l: 86, t: 30.5, w: 14, h: 15.5 }, // 8 右外下 - { l: 10, t: 47, w: 13, h: 16 }, // 9 左下收腰 - { l: 24, t: 47, w: 52, h: 16 }, // 10 中下横 - { l: 77, t: 47, w: 13, h: 16 }, // 11 右下收腰 - { l: 38, t: 64, w: 24, h: 14 }, // 12 心尖 + { l: 6, t: 0, w: 38, h: 14, r: '50% 50% 6px 6px' }, // 0 左上叶(半圆顶) + { l: 56, t: 0, w: 38, h: 14, r: '50% 50% 6px 6px' }, // 1 右上叶(半圆顶) + { l: 0, t: 14.8, w: 13, h: 15, r: '55% 6px 6px 6px' }, // 2 左外上(肩部外弧) + { l: 0, t: 30.6, w: 13, h: 14.6, r: '6px 6px 6px 55%' }, // 3 左外下(收腰起弧) + { l: 14, t: 14.8, w: 26, h: 30.4 }, // 4 左大图 + { l: 41, t: 14.8, w: 18, h: 30.4 }, // 5 中轴 + { l: 60, t: 14.8, w: 26, h: 30.4 }, // 6 右大图 + { l: 87, t: 14.8, w: 13, h: 15, r: '6px 55% 6px 6px' }, // 7 右外上(肩部外弧) + { l: 87, t: 30.6, w: 13, h: 14.6, r: '6px 6px 55% 6px' }, // 8 右外下(收腰起弧) + { l: 8, t: 46.2, w: 16, h: 16, r: '6px 6px 6px 65%' }, // 9 左下收腰(底外弧) + { l: 25, t: 46.2, w: 50, h: 16 }, // 10 中下横 + { l: 76, t: 46.2, w: 16, h: 16, r: '6px 6px 65% 6px' }, // 11 右下收腰(底外弧) + { l: 34, t: 63, w: 32, h: 16.5, clip: 'polygon(0 0, 100% 0, 50% 100%)' }, // 12 心尖(V 形) ] export const HEART_SLOT_COUNT = HEART_SLOTS.length @@ -37,6 +40,16 @@ export function heartSlotLabel(i) { return HEART_SLOT_LABELS[i] || `图${i + 1}` } +/** 格子形状样式(圆角/裁切),作用在展示格上让整体轮廓贴近心形 */ +export function heartCellShape(i) { + const s = HEART_SLOTS[i] + if (!s) return {} + const style = {} + if (s.r) style.borderRadius = s.r + if (s.clip) style.clipPath = s.clip + return style +} + export function heartFocusAt(page, i, axis = 'x') { const arr = axis === 'y' ? page?.imagesFocusY : page?.imagesFocusX const v = Array.isArray(arr) ? arr[i] : 50 diff --git a/vite-tailwindcss/src/composables/posterDefaults.js b/vite-tailwindcss/src/composables/posterDefaults.js index 6ffc8a9..2126e4f 100644 --- a/vite-tailwindcss/src/composables/posterDefaults.js +++ b/vite-tailwindcss/src/composables/posterDefaults.js @@ -16,6 +16,48 @@ export const POSTER_TEMPLATES = [ { value: 'split', label: '左图右文', desc: '竖排书法 + 右侧宴席信息' }, ] +/** + * 海报风格模板(5 套配色皮肤,竖版/横版通用) + * - deep/mid/gold 供后台色卡预览 + * - sealColor/sealFill:canvas 囍字印章用色(canvas 不认 CSS 变量) + * - shotBg:保存长图(html2canvas)的底色 + */ +export const POSTER_THEMES = [ + { + value: 'redgold', label: '朱红鎏金', desc: '经典中国红 + 鎏金描边', + deep: '#7a0000', mid: '#9b0000', gold: '#f0d78c', + sealColor: '#d4af37', sealFill: 'rgba(155, 0, 0, 0.5)', shotBg: '#7a0000', + }, + { + value: 'burgundy', label: '酒红丝绒', desc: '深酒红绒面 + 玫瑰金', + deep: '#4a1220', mid: '#6b1d30', gold: '#e2b98f', + sealColor: '#d9a05b', sealFill: 'rgba(90, 22, 40, 0.55)', shotBg: '#4a1220', + }, + { + value: 'navygold', label: '藏蓝烫金', desc: '午夜藏蓝 + 亮金烫印', + deep: '#101f38', mid: '#1a2f52', gold: '#e8cf96', + sealColor: '#d4b060', sealFill: 'rgba(16, 31, 56, 0.6)', shotBg: '#101f38', + }, + { + value: 'inkjade', label: '黛绿描金', desc: '墨玉黛绿 + 青金描线', + deep: '#132a24', mid: '#1d3a32', gold: '#d8c493', + sealColor: '#c9b37e', sealFill: 'rgba(19, 42, 36, 0.6)', shotBg: '#132a24', + }, + { + value: 'rosewood', label: '绯梅胭脂', desc: '绯梅粉绒 + 香槟金', + deep: '#5c1f30', mid: '#7d2c42', gold: '#f0d3b2', + sealColor: '#e0b487', sealFill: 'rgba(110, 38, 58, 0.55)', shotBg: '#5c1f30', + }, +] + +export function posterThemeMeta(value) { + return POSTER_THEMES.find((t) => t.value === value) || POSTER_THEMES[0] +} + +export function normalizePosterTheme(value) { + return POSTER_THEMES.some((t) => t.value === value) ? value : 'redgold' +} + /** 手机端布局:仅影响窄屏;PC 仍按模板本身版式 */ export const MOBILE_LAYOUTS = [ { value: 'auto', label: '自动布局', desc: '按屏幕宽度自动切换(当前默认)' }, @@ -162,6 +204,7 @@ function baseFields(side) { side: resolvePosterSide(side) || 1, layout: 1, template: 'classic', + posterTheme: 'redgold', mobileLayout: 'auto', enabled: true, heroImg: '', @@ -395,7 +438,7 @@ const SHARED_COPY_KEYS = [ 'musicUrl', 'invitePath', 'inviteButtonText', 'showInviteButton', 'enabled', 'pageTitle', 'pageDescription', 'footer', 'title', 'greeting', 'verticalSlogan', 'subSlogan1', 'subSlogan2', 'loveFooter', - 'loadingEffect', 'enterEffect', + 'loadingEffect', 'enterEffect', 'posterTheme', ] function copySharedFields(from, into) { @@ -432,6 +475,7 @@ export function normalizePosterConfig(raw, side = 1, layout = 1) { side: s, layout: l, template, + posterTheme: normalizePosterTheme(loaded.posterTheme), mobileLayout, lines: lines.length ? lines : base.lines, groomName, diff --git a/vite-tailwindcss/src/composables/resizeImage.js b/vite-tailwindcss/src/composables/resizeImage.js index 05b33df..5764761 100644 --- a/vite-tailwindcss/src/composables/resizeImage.js +++ b/vite-tailwindcss/src/composables/resizeImage.js @@ -238,6 +238,18 @@ export function getSlotResizeSpecs(page) { boxForIndex = () => ({ maxWidth: 1200, maxHeight: 780 }) } else if (page.type === 'polaroid') { boxForIndex = () => ({ maxWidth: 800, maxHeight: 1000 }) + } else if (page.type === 'triptych') { + // 窄竖条(10:17) + boxForIndex = () => ({ maxWidth: 560, maxHeight: 960 }) + } else if (page.type === 'filmstrip') { + // 横滑大帧(4:5) + boxForIndex = () => ({ maxWidth: 900, maxHeight: 1125 }) + } else if (page.type === 'fan') { + // 扇形卡(3:4) + boxForIndex = () => ({ maxWidth: 700, maxHeight: 940 }) + } else if (page.type === 'mosaic') { + // 横竖混排格 + boxForIndex = () => ({ maxWidth: 900, maxHeight: 900 }) } else if (page.type === 'heart-collage') { // 按槽位比例:大图 / 竖中轴 / 横图 / 小方图 boxForIndex = (i) => { diff --git a/vite-tailwindcss/src/directives/reveal.js b/vite-tailwindcss/src/directives/reveal.js index 216ba9b..3549e64 100644 --- a/vite-tailwindcss/src/directives/reveal.js +++ b/vite-tailwindcss/src/directives/reveal.js @@ -7,7 +7,7 @@ * v-reveal="{ variant: 'scale', delay: 200 }" * v-reveal="{ variant: 'up', immediate: true }" -> 不建观察器,下一帧直接入场(串行挂载用) * - * 支持的 variant: up | down | left | right | scale | blur + * 支持的 variant: up | down | left | right | scale | blur | postcard */ const VARIANTS = { up: 'reveal-up', @@ -16,6 +16,7 @@ const VARIANTS = { right: 'reveal-right', scale: 'reveal-scale', blur: 'reveal-blur', + postcard: 'reveal-postcard', } function parseBinding(value) { diff --git a/vite-tailwindcss/src/main.js b/vite-tailwindcss/src/main.js index be13f1b..ff6ee07 100644 --- a/vite-tailwindcss/src/main.js +++ b/vite-tailwindcss/src/main.js @@ -4,6 +4,7 @@ import Antd from 'ant-design-vue' import './style.css' import './styles/animations.css' import './styles/frames.css' +import './styles/theme-packs.css' import App from './App.vue' import '@fortawesome/fontawesome-free/css/all.css' // 引入所有样式 // 注册路由 diff --git a/vite-tailwindcss/src/style.css b/vite-tailwindcss/src/style.css index bd8202a..c2ed8c7 100644 --- a/vite-tailwindcss/src/style.css +++ b/vite-tailwindcss/src/style.css @@ -9,6 +9,264 @@ @import "@fontsource/pinyon-script/400.css"; @import "@fontsource/cinzel/400.css"; @import "@fontsource/cinzel/700.css"; +@import "@fontsource/fusion-pixel-12px-proportional-sc/400.css"; + +/* ============================================================ + 整站风格模板(前后台共用):html[data-theme] 切换整组变量 + 色板: + - --c-primary / --c-primary-deep / --c-primary-rgb 主品牌色 + - --c-line / --c-line-rgb 线条/描边 + - --c-muted / --c-ink 次要字 / 正文 + - --c-paper / --c-bg / --c-bg-deep 卡纸 / 页面底 / 深底 + - --c-card 浮层卡片底(暗色主题为深卡) + - --c-on-primary 主色底上的文字色 + 风格: + - --t-font-title / --t-font-kicker / --t-font-body 标题 / 眉题 / 正文字体 + - --t-radius-card / --t-radius-btn 卡片 / 按钮圆角 + - --t-bg-texture 页面背景纹理(none 或 SVG) + ============================================================ */ +:root, +html[data-theme='champagne'] { + --c-primary: #a88c6b; + --c-primary-deep: #967b5c; + --c-primary-rgb: 168, 140, 107; + --c-line: #b89a78; + --c-line-rgb: 184, 154, 120; + --c-muted: #8a8680; + --c-ink: #2c2c2c; + --c-ink-soft: #5c4a35; + --c-paper: #fdfbf7; + --c-bg: #fbf6ef; + --c-bg-deep: #f3eee6; + --c-card: #ffffff; + --c-on-primary: #ffffff; + --t-font-title: var(--font-calligraphy); + --t-font-kicker: var(--font-calligraphy); + --t-font-body: var(--font-sans); + --t-radius-card: 12px; + --t-radius-btn: 999px; + --t-bg-texture: none; +} +/* 新中式:黛绿朱砂 · 宋体留白 · 竹枝细纹 */ +html[data-theme='guofeng-new'] { + --c-primary: #486156; + --c-primary-deep: #395046; + --c-primary-rgb: 72, 97, 86; + --c-line: #b0885a; + --c-line-rgb: 176, 136, 90; + --c-muted: #8b8a80; + --c-ink: #2c302c; + --c-ink-soft: #4a4f48; + --c-paper: #faf8f2; + --c-bg: #f6f3ec; + --c-bg-deep: #ece7db; + --c-card: #fffefa; + --c-on-primary: #f6f3ec; + --t-font-title: var(--font-serif); + --t-font-kicker: var(--font-kai); + --t-font-body: var(--font-serif); + --t-radius-card: 2px; + --t-radius-btn: 2px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cg fill='none' stroke='%23486156' stroke-opacity='0.04' stroke-width='1'%3E%3Cpath d='M24 96q2-26-6-44M24 74q10-4 14-14M24 60q-9-2-13-10'/%3E%3C/g%3E%3Cg fill='%23b5493f' fill-opacity='0.05'%3E%3Crect x='92' y='22' width='7' height='7'/%3E%3C/g%3E%3C/svg%3E"); +} +/* 老中式:朱红鎏金 · 婚书囍字 · 宣纸噪点 */ +html[data-theme='guofeng-old'] { + --c-primary: #a8323e; + --c-primary-deep: #8c2833; + --c-primary-rgb: 168, 50, 62; + --c-line: #c9a24a; + --c-line-rgb: 201, 162, 74; + --c-muted: #9a8578; + --c-ink: #3a2428; + --c-ink-soft: #5c3a40; + --c-paper: #fdf6f0; + --c-bg: #f9efe6; + --c-bg-deep: #f2e3d4; + --c-card: #fffaf4; + --c-on-primary: #f7e7c3; + --t-font-title: var(--font-mashan); + --t-font-kicker: var(--font-kai); + --t-font-body: var(--font-serif); + --t-radius-card: 4px; + --t-radius-btn: 4px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cg fill='%238c2833' fill-opacity='0.025'%3E%3Ccircle cx='12' cy='20' r='1'/%3E%3Ccircle cx='48' cy='8' r='1.4'/%3E%3Ccircle cx='76' cy='34' r='1'/%3E%3Ccircle cx='30' cy='52' r='1.2'/%3E%3Ccircle cx='64' cy='70' r='1'/%3E%3Ccircle cx='10' cy='80' r='1.3'/%3E%3Ccircle cx='86' cy='84' r='1'/%3E%3C/g%3E%3C/svg%3E"); +} +/* 法式浪漫:奶油粉白 · 花体 · 细水彩点 */ +html[data-theme='french'] { + --c-primary: #c78a96; + --c-primary-deep: #b57683; + --c-primary-rgb: 199, 138, 150; + --c-line: #d9aab4; + --c-line-rgb: 217, 170, 180; + --c-muted: #9c8b90; + --c-ink: #3d3136; + --c-ink-soft: #5c454e; + --c-paper: #fdfafa; + --c-bg: #fdf6f4; + --c-bg-deep: #f7e9e7; + --c-card: #ffffff; + --c-on-primary: #ffffff; + --t-font-title: var(--font-calligraphy); + --t-font-kicker: "Pinyon Script", cursive; + --t-font-body: var(--font-sans); + --t-radius-card: 18px; + --t-radius-btn: 999px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cg fill='%23c78a96' fill-opacity='0.03'%3E%3Ccircle cx='24' cy='30' r='7'/%3E%3Ccircle cx='88' cy='16' r='5'/%3E%3Ccircle cx='104' cy='78' r='8'/%3E%3Ccircle cx='40' cy='96' r='6'/%3E%3C/g%3E%3C/svg%3E"); +} +/* 现代简约:暖灰低饱和 · 无衬线 · 细几何 */ +html[data-theme='modern'] { + --c-primary: #8a8578; + --c-primary-deep: #757064; + --c-primary-rgb: 138, 133, 120; + --c-line: #a39e91; + --c-line-rgb: 163, 158, 145; + --c-muted: #8f8c85; + --c-ink: #2e2d2a; + --c-ink-soft: #4a4842; + --c-paper: #fbfaf8; + --c-bg: #f7f6f3; + --c-bg-deep: #edebe6; + --c-card: #ffffff; + --c-on-primary: #ffffff; + --t-font-title: var(--font-sans); + --t-font-kicker: var(--font-sans); + --t-font-body: var(--font-sans); + --t-radius-card: 8px; + --t-radius-btn: 10px; + --t-bg-texture: none; +} +/* 森系自然:青绿 · 手写 · 纸纹 */ +html[data-theme='forest'] { + --c-primary: #6f9678; + --c-primary-deep: #5d8266; + --c-primary-rgb: 111, 150, 120; + --c-line: #92b399; + --c-line-rgb: 146, 179, 153; + --c-muted: #86928a; + --c-ink: #29322c; + --c-ink-soft: #3d4a40; + --c-paper: #f9fcf9; + --c-bg: #f3f8f2; + --c-bg-deep: #e9f0e7; + --c-card: #ffffff; + --c-on-primary: #ffffff; + --t-font-title: var(--font-longcang); + --t-font-kicker: var(--font-liujian); + --t-font-body: var(--font-sans); + --t-radius-card: 14px; + --t-radius-btn: 999px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='110' height='110'%3E%3Cg fill='none' stroke='%235d8266' stroke-opacity='0.035' stroke-width='1'%3E%3Cpath d='M20 30q8-12 18-6M74 80q10-10 20-4M90 24q6 10-4 16M34 92q-10 4-14-6'/%3E%3C/g%3E%3C/svg%3E"); +} +/* 星空夜色:深蓝烫金 · 暗色主题 · 星点 */ +html[data-theme='night'] { + --c-primary: #c9a96a; + --c-primary-deep: #b3945a; + --c-primary-rgb: 201, 169, 106; + --c-line: #8fa3c0; + --c-line-rgb: 143, 163, 192; + --c-muted: #93a0b5; + --c-ink: #e8e6df; + --c-ink-soft: #cfd4de; + --c-paper: #16233c; + --c-bg: #101b2e; + --c-bg-deep: #0a1322; + --c-card: #1a2946; + --c-on-primary: #14213a; + --t-font-title: var(--font-display); + --t-font-kicker: var(--font-cinzel); + --t-font-body: var(--font-sans); + --t-radius-card: 10px; + --t-radius-btn: 999px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cg fill='%23e8e6df'%3E%3Ccircle cx='16' cy='24' r='1' fill-opacity='0.3'/%3E%3Ccircle cx='72' cy='10' r='0.8' fill-opacity='0.2'/%3E%3Ccircle cx='120' cy='40' r='1.2' fill-opacity='0.34'/%3E%3Ccircle cx='44' cy='66' r='0.7' fill-opacity='0.18'/%3E%3Ccircle cx='98' cy='92' r='1' fill-opacity='0.26'/%3E%3Ccircle cx='20' cy='112' r='0.9' fill-opacity='0.22'/%3E%3Ccircle cx='132' cy='126' r='0.8' fill-opacity='0.2'/%3E%3C/g%3E%3C/svg%3E"); +} +/* 杂志封面:高对比黑棕 · Didot 大字 · 方角 */ +html[data-theme='magazine'] { + --c-primary: #1f1c19; + --c-primary-deep: #000000; + --c-primary-rgb: 31, 28, 25; + --c-line: #8a8378; + --c-line-rgb: 138, 131, 120; + --c-muted: #7d786f; + --c-ink: #1f1c19; + --c-ink-soft: #3a352e; + --c-paper: #faf8f4; + --c-bg: #f5f2ec; + --c-bg-deep: #eae6dd; + --c-card: #ffffff; + --c-on-primary: #ffffff; + --t-font-title: var(--font-display); + --t-font-kicker: var(--font-cinzel); + --t-font-body: var(--font-sans); + --t-radius-card: 0px; + --t-radius-btn: 0px; + --t-bg-texture: none; +} +/* 日报编辑:报纸米黄 · 衬线排印 · 直角细框 */ +html[data-theme='newspaper'] { + --c-primary: #2b2822; + --c-primary-deep: #14120e; + --c-primary-rgb: 43, 40, 34; + --c-line: #6b675e; + --c-line-rgb: 107, 103, 94; + --c-muted: #6f6b62; + --c-ink: #262420; + --c-ink-soft: #423e35; + --c-paper: #fbf9f2; + --c-bg: #f6f3ea; + --c-bg-deep: #ebe7da; + --c-card: #fffdf6; + --c-on-primary: #fbf9f2; + --t-font-title: var(--font-serif); + --t-font-kicker: var(--font-display); + --t-font-body: var(--font-serif); + --t-radius-card: 0px; + --t-radius-btn: 2px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6' height='6'%3E%3Crect x='0' y='0' width='1' height='1' fill='%232b2822' fill-opacity='0.016'/%3E%3C/svg%3E"); +} +/* 复古像素:糖果撞色 · 像素字 · 棋盘点 */ +html[data-theme='pixel'] { + --c-primary: #c94f7c; + --c-primary-deep: #b03e69; + --c-primary-rgb: 201, 79, 124; + --c-line: #5f9ea0; + --c-line-rgb: 95, 158, 160; + --c-muted: #8a7f74; + --c-ink: #33261f; + --c-ink-soft: #54433a; + --c-paper: #fdf9ee; + --c-bg: #faf3e6; + --c-bg-deep: #f0e6d2; + --c-card: #fffdf4; + --c-on-primary: #fffdf4; + --t-font-title: var(--font-pixel); + --t-font-kicker: var(--font-pixel); + --t-font-body: var(--font-pixel); + --t-radius-card: 0px; + --t-radius-btn: 0px; + --t-bg-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Crect x='0' y='0' width='4' height='4' fill='%2333261f' fill-opacity='0.02'/%3E%3Crect x='4' y='4' width='4' height='4' fill='%2333261f' fill-opacity='0.02'/%3E%3C/svg%3E"); +} +/* 极简主义:黑白留白 · 细线 · 轴线 */ +html[data-theme='zen'] { + --c-primary: #2a2a28; + --c-primary-deep: #111111; + --c-primary-rgb: 42, 42, 40; + --c-line: #8f8d88; + --c-line-rgb: 143, 141, 136; + --c-muted: #85837e; + --c-ink: #262624; + --c-ink-soft: #444442; + --c-paper: #fdfdfc; + --c-bg: #fafaf8; + --c-bg-deep: #f0f0ed; + --c-card: #ffffff; + --c-on-primary: #ffffff; + --t-font-title: var(--font-sans); + --t-font-kicker: var(--font-sans); + --t-font-body: var(--font-sans); + --t-radius-card: 0px; + --t-radius-btn: 0px; + --t-bg-texture: none; +} :root { --font-sans: "Avenir Next", "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif; @@ -22,6 +280,8 @@ --font-liujian: "Liu Jian Mao Cao", "Zhi Mang Xing", cursive, serif; --font-cinzel: "Cinzel", "Playfair Display", serif; --font-number: "Cochin", "Georgia", "Times New Roman", serif; + /* CDN 全量中日韩像素字优先(按 unicode-range 分片下载),本地 latin 子集兜底 */ + --font-pixel: "Fusion Pixel 12px Mono latin", "Fusion Pixel 12px Proportional SC", "Courier New", monospace; } @@ -32,6 +292,19 @@ body { overflow-anchor: none; } +/* ---------- 后台实时预览(?preview=1 嵌入手机壳):隐藏一切滚动条,模拟真机 ---------- */ +html.preview-mode, +html.preview-mode * { + scrollbar-width: none; + -ms-overflow-style: none; +} +html.preview-mode::-webkit-scrollbar, +html.preview-mode *::-webkit-scrollbar { + display: none; + width: 0; + height: 0; +} + .calligraphy { font-family: var(--font-calligraphy) !important; } @@ -44,3 +317,18 @@ body { font-family: var(--font-calligraphy) !important; font-weight: 700; } + +/* ---------- 图片预览灯箱(Ant Design 全局覆写:遮罩柔化 + 操作栏磨砂) ---------- */ +.ant-image-preview-mask { + background: rgba(30, 24, 17, 0.72) !important; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} +.ant-image-preview-operations { + background: rgba(24, 19, 14, 0.45) !important; + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); +} +.ant-image-preview-img { + border-radius: 4px; +} diff --git a/vite-tailwindcss/src/styles/animations.css b/vite-tailwindcss/src/styles/animations.css index 2a5cf5e..29822b7 100644 --- a/vite-tailwindcss/src/styles/animations.css +++ b/vite-tailwindcss/src/styles/animations.css @@ -18,11 +18,56 @@ .reveal-right { transform: translateX(56px); } .reveal-scale { transform: scale(0.9); } .reveal-blur { transform: scale(1.04); filter: blur(14px); } +/* 明信片飞入:带轻微旋转,弹性回正(单图明信片/异形边框用) */ +.reveal-postcard { + transform: translateY(70px) rotate(-7deg) scale(0.94); + transform-origin: 50% 80%; + transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); +} .reveal--in { opacity: 1; transform: none; filter: none; } @media (prefers-reduced-motion: reduce) { - .reveal { opacity: 1 !important; transform: none !important; filter: none !important; transition: none; } + /* filter 只清 blur 变体:同元素可能还有 .gf-shape-shadow 的 drop-shadow */ + .reveal { opacity: 1 !important; transform: none !important; transition: none; } + .reveal-blur { filter: none !important; } +} + +/* ---------- 邮戳盖章(单图 明信片/邮票 边框的入场收尾) ---------- */ +.gf-postmark { + position: absolute; + top: 16px; + right: 14px; + width: 56px; + height: 56px; + border-radius: 50%; + border: 2px solid rgba(187, 72, 62, 0.85); + outline: 1px solid rgba(187, 72, 62, 0.5); + outline-offset: -7px; + color: rgba(187, 72, 62, 0.9); + display: flex; + align-items: center; + justify-content: center; + font-family: var(--font-calligraphy, serif); + font-size: 22px; + line-height: 1; + transform: rotate(-14deg); + opacity: 0; + pointer-events: none; + z-index: 2; +} +.gf-postmark::before { content: '囍'; } +.reveal--in .gf-postmark { + animation: postmarkStamp 0.55s cubic-bezier(0.2, 1.4, 0.45, 1) 0.65s both; +} +@keyframes postmarkStamp { + 0% { opacity: 0; transform: rotate(-14deg) scale(1.9); } + 55% { opacity: 0.95; transform: rotate(-14deg) scale(0.92); } + 75% { transform: rotate(-14deg) scale(1.05); } + 100% { opacity: 0.9; transform: rotate(-14deg) scale(1); } +} +@media (prefers-reduced-motion: reduce) { + .reveal--in .gf-postmark { animation: none; opacity: 0.9; } } /* ---------- 飘落花瓣(优雅:柔和渐变 + 轻微摇曳) ---------- */ @@ -132,7 +177,7 @@ /* ---------- 渐变流光文字 ---------- */ @keyframes gradientShift { to { background-position: 200% center; } } .gradient-text { - background: linear-gradient(90deg, #a88c6b, #e8a5b2, #d4af37, #a88c6b); + background: linear-gradient(90deg, var(--c-primary), #e8a5b2, #d4af37, var(--c-primary)); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; @@ -160,7 +205,7 @@ 100% { background-position: 200% 0; } } .shimmer-line { - background: linear-gradient(90deg, transparent, rgba(168,140,107,0.6), transparent); + background: linear-gradient(90deg, transparent, rgba(var(--c-primary-rgb),0.6), transparent); background-size: 200% 100%; animation: shimmer 3s linear infinite; } diff --git a/vite-tailwindcss/src/styles/frames.css b/vite-tailwindcss/src/styles/frames.css index 491c6a0..a379275 100644 --- a/vite-tailwindcss/src/styles/frames.css +++ b/vite-tailwindcss/src/styles/frames.css @@ -117,6 +117,586 @@ background: #fff; } +/* ============================================================ + 明信片 · 异形边框(group: postcard) + - mask/clip 裁切类(stamp/scallop/torn/ticket/heart)box-shadow 会被 + mask 一并裁掉,投影统一由父级 .gf-shape-shadow 的 drop-shadow 提供 + - 装饰伪元素需要定位上下文:用零优先级 :where() 设 relative, + 模板里 Tailwind 的 .absolute(0,1,0)永远能赢,不违背头部约定 + ============================================================ */ +:where(.gf-postcard, .gf-film, .gf-stamp, .gf-arch, .gf-oval, .gf-circle, .gf-leaf, +.gf-heart, .gf-flower, .gf-hexagon, .gf-scallop, .gf-wave, .gf-zigzag, +.gf-torn, .gf-ticket, .gf-bevel, +.gf-cloud, .gf-mist, .gf-bloom, .gf-panorama, .gf-ovalwide) { + position: relative; +} + +/* 边缘渐隐内晕:照片边缘向纸色柔和过渡,与边框/裁切形状融为一体 + (mask/clip 类的伪元素会随形状一并裁切,晕自动贴合轮廓) */ +.gf-heart::after, .gf-flower::after, .gf-scallop::after, .gf-wave::after, +.gf-zigzag::after, .gf-torn::after, .gf-stamp::after, .gf-ticket::after, +.gf-hexagon::after, .gf-bevel::after, .gf-arch::after, .gf-oval::after, +.gf-circle::after, .gf-leaf::after, .gf-cloud::after, .gf-ovalwide::after { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(ellipse 62% 62% at 50% 50%, rgba(255, 252, 246, 0) 55%, rgba(255, 252, 246, 0.48) 100%); + pointer-events: none; +} + +/* 邮票齿孔(四边半圆打孔,经典邮票纸边) */ +.gf-stamp { + --gf-r: 5px; + padding: 12px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + -webkit-mask: + radial-gradient(var(--gf-r), #0000 98%, #000) calc(-1.5 * var(--gf-r)) calc(-1.5 * var(--gf-r)) / calc(3 * var(--gf-r)) calc(3 * var(--gf-r)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 3 * var(--gf-r)) calc(100% - 3 * var(--gf-r)) no-repeat; + mask: + radial-gradient(var(--gf-r), #0000 98%, #000) calc(-1.5 * var(--gf-r)) calc(-1.5 * var(--gf-r)) / calc(3 * var(--gf-r)) calc(3 * var(--gf-r)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 3 * var(--gf-r)) calc(100% - 3 * var(--gf-r)) no-repeat; +} +/* 邮票内框线(贴照片边缘内侧,经典邮票图框) */ +.gf-stamp::before { + content: ''; + position: absolute; + inset: 15px; + border: 1px solid rgba(255, 255, 255, 0.65); + pointer-events: none; + z-index: 1; +} + +/* 复古明信片(暖白卡 + POST CARD 字样 + 邮戳/邮票装饰) */ +.gf-postcard { + padding: 10px 10px 40px; + border: 1px solid rgba(184, 154, 120, 0.28); + border-radius: 4px; + box-shadow: 0 10px 26px rgba(120, 90, 60, 0.16); + background: #fffdf6; +} +.gf-postcard::before { + content: 'POST CARD'; + position: absolute; + left: 14px; + bottom: 14px; + /* 窄格(九宫格/爱心拼贴)里给右侧邮戳让位,calc 为负时按 0 处理自动隐藏 */ + max-width: calc(100% - 118px); + overflow: hidden; + white-space: nowrap; + font-family: Georgia, 'Times New Roman', serif; + font-size: 9px; + line-height: 1; + letter-spacing: 0.35em; + color: #b89a78; + pointer-events: none; +} +.gf-postcard::after { + content: ''; + position: absolute; + right: 8px; + bottom: 3px; + width: 96px; + height: 36px; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 48'%3E%3Cg fill='none' stroke='%23b89a78' stroke-width='1.5'%3E%3Ccircle cx='24' cy='24' r='15'/%3E%3Ccircle cx='24' cy='24' r='10.5'/%3E%3Cpath d='M48 14c5-3 9 3 14 0M48 22c5-3 9 3 14 0M48 30c5-3 9 3 14 0' stroke-width='1.2'/%3E%3C/g%3E%3Cg transform='translate(76,6)'%3E%3Crect width='30' height='36' fill='none' stroke='%23c9a978' stroke-width='1.5' stroke-dasharray='3 2.4'/%3E%3Cpath d='M15 26c-5-3.6-9-7-9-11a5 5 0 0 1 9-2.6a5 5 0 0 1 9 2.6c0 4-4 7.4-9 11z' fill='%23d9a0a5'/%3E%3C/g%3E%3C/svg%3E") no-repeat right center / contain; + opacity: 0.9; + pointer-events: none; +} + +/* 拱门(顶部全圆,婚礼穹顶造型) */ +.gf-arch { + padding: 8px; + border: 1px solid rgba(201, 169, 120, 0.5); + border-radius: 999px 999px 14px 14px; + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.65), 0 14px 32px rgba(120, 90, 60, 0.16); + background: #fff; +} +.gf-arch > img { + border-radius: 992px 992px 8px 8px; +} +/* 拱顶花饰圆章(呼应尾页门框 keystone) */ +.gf-arch::before { + content: '❀'; + position: absolute; + top: 12px; + left: 50%; + transform: translateX(-50%); + width: 26px; + height: 26px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + line-height: 1; + color: #b08e63; + background: rgba(253, 251, 247, 0.92); + box-shadow: 0 0 0 1px rgba(201, 169, 120, 0.45), 0 2px 6px rgba(120, 90, 60, 0.18); + pointer-events: none; + z-index: 1; +} + +/* 椭圆相框 */ +.gf-oval { + padding: 7px; + border: 1px solid rgba(201, 169, 120, 0.55); + border-radius: 50%; + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.7), 0 12px 30px rgba(120, 90, 60, 0.16); + background: #fff; +} +.gf-oval > img { + border-radius: 50%; +} + +/* 正圆相框(推荐 1:1) */ +.gf-circle { + padding: 8px; + border: 1px solid rgba(201, 169, 120, 0.55); + border-radius: 50%; + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.7), 0 0 0 5px rgba(201, 169, 120, 0.35), 0 12px 30px rgba(120, 90, 60, 0.16); + background: #fff; +} +.gf-circle > img { + border-radius: 50%; +} +/* 椭圆/正圆:底部花饰小章 */ +.gf-oval::before, +.gf-circle::before { + content: '❀'; + position: absolute; + bottom: 10px; + left: 50%; + transform: translateX(-50%); + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + line-height: 1; + color: #b08e63; + background: rgba(253, 251, 247, 0.92); + box-shadow: 0 0 0 1px rgba(201, 169, 120, 0.4), 0 2px 5px rgba(120, 90, 60, 0.15); + pointer-events: none; + z-index: 1; +} + +/* 叶形花瓣(两对角大圆两对角小圆,推荐 1:1) */ +.gf-leaf { + padding: 7px; + border: 1px solid rgba(201, 169, 120, 0.5); + border-radius: 60% 12% 60% 12%; + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.6), 0 12px 30px rgba(120, 90, 60, 0.15); + background: #fff; +} +.gf-leaf > img { + border-radius: 58% 9% 58% 9%; +} + +/* 六边形(clip 直裁,推荐 1:1) */ +.gf-hexagon { + padding: 0; + border: 0; + border-radius: 0; + box-shadow: none; + clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); + -webkit-clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); +} + +/* 八瓣花(多层 radial mask 合成花形,推荐 1:1) */ +.gf-flower { + padding: 0; + border: 0; + border-radius: 0; + box-shadow: none; + -webkit-mask: + radial-gradient(19% 19% at 50% 19%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 71.9% 28.1%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 81% 50%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 71.9% 71.9%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 50% 81%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 28.1% 71.9%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 19% 50%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 28.1% 28.1%, #000 98%, #0000 100%), + radial-gradient(34% 34% at 50% 50%, #000 98%, #0000 100%); + mask: + radial-gradient(19% 19% at 50% 19%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 71.9% 28.1%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 81% 50%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 71.9% 71.9%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 50% 81%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 28.1% 71.9%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 19% 50%, #000 98%, #0000 100%), + radial-gradient(19% 19% at 28.1% 28.1%, #000 98%, #0000 100%), + radial-gradient(34% 34% at 50% 50%, #000 98%, #0000 100%); +} + +/* 心形(SVG mask 裁切,推荐 1:1) */ +.gf-heart { + padding: 0; + border: 0; + border-radius: 0; + box-shadow: none; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23000' d='M462.3 62.6C407.5 15.9 326 24.3 275.7 76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1 53-154.3-9.8-207.9z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%; +} + +/* 扇贝花边(四边外凸圆弧波浪) */ +.gf-scallop { + --gf-r: 7px; + padding: 14px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + -webkit-mask: + radial-gradient(var(--gf-r), #000 98%, #0000) 0 0 / calc(2 * var(--gf-r)) calc(2 * var(--gf-r)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 2 * var(--gf-r)) calc(100% - 2 * var(--gf-r)) no-repeat; + mask: + radial-gradient(var(--gf-r), #000 98%, #0000) 0 0 / calc(2 * var(--gf-r)) calc(2 * var(--gf-r)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 2 * var(--gf-r)) calc(100% - 2 * var(--gf-r)) no-repeat; +} +/* 蕾丝缝线(内圈虚线) */ +.gf-scallop::before { + content: ''; + position: absolute; + inset: 17px; + border: 1px dashed rgba(255, 255, 255, 0.7); + border-radius: 2px; + pointer-events: none; + z-index: 1; +} + +/* 蕾丝波浪(四边相切凹圆弧,剪纸蕾丝垫感) */ +.gf-wave { + --gf-r: 9px; + padding: 15px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + -webkit-mask: + radial-gradient(var(--gf-r), #0000 98%, #000) calc(-1 * var(--gf-r)) calc(-1 * var(--gf-r)) / calc(2 * var(--gf-r)) calc(2 * var(--gf-r)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 2 * var(--gf-r)) calc(100% - 2 * var(--gf-r)) no-repeat; + mask: + radial-gradient(var(--gf-r), #0000 98%, #000) calc(-1 * var(--gf-r)) calc(-1 * var(--gf-r)) / calc(2 * var(--gf-r)) calc(2 * var(--gf-r)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 2 * var(--gf-r)) calc(100% - 2 * var(--gf-r)) no-repeat; +} +/* 蕾丝点状缝线 */ +.gf-wave::before { + content: ''; + position: absolute; + inset: 18px; + border: 1px dotted rgba(255, 255, 255, 0.75); + border-radius: 3px; + pointer-events: none; + z-index: 1; +} + +/* 锯齿花边(四边三角齿,花边剪刀裁切的相纸;齿深须为齿宽一半) */ +.gf-zigzag { + --gf-z: 16px; + --gf-zd: 8px; + padding: 14px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + -webkit-mask: + conic-gradient(from 135deg at 50% 0, #000 90deg, #0000 0) 50% 0 / var(--gf-z) var(--gf-zd) repeat-x, + conic-gradient(from -45deg at 50% 100%, #000 90deg, #0000 0) 50% 100% / var(--gf-z) var(--gf-zd) repeat-x, + conic-gradient(from 45deg at 0 50%, #000 90deg, #0000 0) 0 50% / var(--gf-zd) var(--gf-z) repeat-y, + conic-gradient(from 225deg at 100% 50%, #000 90deg, #0000 0) 100% 50% / var(--gf-zd) var(--gf-z) repeat-y, + linear-gradient(#000 0 0) 50% / calc(100% - 2 * var(--gf-zd) + 2px) calc(100% - 2 * var(--gf-zd) + 2px) no-repeat; + mask: + conic-gradient(from 135deg at 50% 0, #000 90deg, #0000 0) 50% 0 / var(--gf-z) var(--gf-zd) repeat-x, + conic-gradient(from -45deg at 50% 100%, #000 90deg, #0000 0) 50% 100% / var(--gf-z) var(--gf-zd) repeat-x, + conic-gradient(from 45deg at 0 50%, #000 90deg, #0000 0) 0 50% / var(--gf-zd) var(--gf-z) repeat-y, + conic-gradient(from 225deg at 100% 50%, #000 90deg, #0000 0) 100% 50% / var(--gf-zd) var(--gf-z) repeat-y, + linear-gradient(#000 0 0) 50% / calc(100% - 2 * var(--gf-zd) + 2px) calc(100% - 2 * var(--gf-zd) + 2px) no-repeat; +} +/* 剪纸内线 */ +.gf-zigzag::before { + content: ''; + position: absolute; + inset: 17px; + border: 1px solid rgba(255, 255, 255, 0.55); + pointer-events: none; + z-index: 1; +} + +/* 切角相纸(八边形切角,复古照片角) */ +.gf-bevel { + padding: 10px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + clip-path: polygon(18px 0, calc(100% - 18px) 0, 100% 18px, 100% calc(100% - 18px), calc(100% - 18px) 100%, 18px 100%, 0 calc(100% - 18px), 0 18px); + -webkit-clip-path: polygon(18px 0, calc(100% - 18px) 0, 100% 18px, 100% calc(100% - 18px), calc(100% - 18px) 100%, 18px 100%, 0 calc(100% - 18px), 0 18px); +} + +/* 撕纸边(不规则毛边,像从相纸上撕下来) */ +.gf-torn { + padding: 12px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23000' d='M0 3L6 1.4L13 3.4L19 1.2L26 3L33 1.6L40 3.2L47 1.3L54 2.8L61 1.5L68 3.3L75 1.4L82 2.9L89 1.6L96 3.1L98.6 8L96.9 14L98.4 20L96.8 27L98.5 34L97 41L98.6 48L96.9 55L98.3 62L97 69L98.5 76L96.8 83L98.4 90L97.1 96L94 98.6L87 97L80 98.7L73 96.9L66 98.5L59 97.1L52 98.6L45 96.8L38 98.4L31 97L24 98.6L17 96.9L10 98.5L4 97.2L1.5 92L3.2 85L1.4 78L3 71L1.6 64L3.3 57L1.3 50L2.9 43L1.5 36L3.2 29L1.4 22L3 15L1.7 8Z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23000' d='M0 3L6 1.4L13 3.4L19 1.2L26 3L33 1.6L40 3.2L47 1.3L54 2.8L61 1.5L68 3.3L75 1.4L82 2.9L89 1.6L96 3.1L98.6 8L96.9 14L98.4 20L96.8 27L98.5 34L97 41L98.6 48L96.9 55L98.3 62L97 69L98.5 76L96.8 83L98.4 90L97.1 96L94 98.6L87 97L80 98.7L73 96.9L66 98.5L59 97.1L52 98.6L45 96.8L38 98.4L31 97L24 98.6L17 96.9L10 98.5L4 97.2L1.5 92L3.2 85L1.4 78L3 71L1.6 64L3.3 57L1.3 50L2.9 43L1.5 36L3.2 29L1.4 22L3 15L1.7 8Z'/%3E%3C/svg%3E") no-repeat 50% 50% / 100% 100%; +} +/* 撕纸:顶部半透明胶带贴纸 */ +.gf-torn::before { + content: ''; + position: absolute; + top: 4px; + left: 50%; + width: 72px; + height: 20px; + transform: translateX(-50%) rotate(-2.5deg); + background: linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.36)); + box-shadow: 0 1px 4px rgba(96, 72, 50, 0.18); + pointer-events: none; + z-index: 1; +} + +/* 车票票根(两侧半圆缺口,缺口自带撕票线意象; + 不画横穿虚线——会压在照片主体/人脸上) */ +.gf-ticket { + --gf-n: 11px; + padding: 10px 16px 26px; + border: 0; + border-radius: 14px; + box-shadow: none; + background: #fff; + -webkit-mask: + radial-gradient(var(--gf-n) at 0 50%, #0000 98%, #000) 0 0 / 51% 100% no-repeat, + radial-gradient(var(--gf-n) at 100% 50%, #0000 98%, #000) 100% 0 / 51% 100% no-repeat; + mask: + radial-gradient(var(--gf-n) at 0 50%, #0000 98%, #000) 0 0 / 51% 100% no-repeat, + radial-gradient(var(--gf-n) at 100% 50%, #0000 98%, #000) 100% 0 / 51% 100% no-repeat; +} +/* 票面字样(位于底部票边区,窄格自动截断居中) */ +.gf-ticket::before { + content: 'ADMIT ONE'; + position: absolute; + left: 0; + right: 0; + bottom: 8px; + text-align: center; + font-family: Georgia, 'Times New Roman', serif; + font-size: 9px; + line-height: 1; + letter-spacing: 0.4em; + text-indent: 0.4em; + color: #b89a78; + white-space: nowrap; + overflow: hidden; + pointer-events: none; +} + +/* 复古胶片(深色框 + 上下齿孔) */ +.gf-film { + padding: 15px 6px; + border: 0; + border-radius: 6px; + box-shadow: 0 12px 28px rgba(30, 24, 18, 0.3); + background: #26231f; +} +.gf-film > img { + border-radius: 2px; +} +.gf-film::before, +.gf-film::after { + content: ''; + position: absolute; + left: 8px; + right: 8px; + height: 7px; + background-image: repeating-linear-gradient(90deg, rgba(253, 251, 247, 0.85) 0 9px, transparent 9px 18px); + pointer-events: none; +} +.gf-film::before { + top: 4px; +} +.gf-film::after { + bottom: 4px; +} + +/* ============================================================ + 横幅 · 云雾花(group: scenic)—— 横向比例的风景明信片 + 装饰主题:云朵 / 晨雾 / 花卉;推荐 4:3 或 16:9 + ============================================================ */ + +/* 云朵相框(大小圆交替蓬松云边,推荐 4:3) */ +.gf-cloud { + --gf-c: 14px; + padding: 16px; + border: 0; + border-radius: 0; + box-shadow: none; + background: #fff; + -webkit-mask: + radial-gradient(var(--gf-c), #000 98%, #0000) 0 0 / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round, + radial-gradient(calc(0.68 * var(--gf-c)), #000 98%, #0000) calc(1.7 * var(--gf-c)) calc(1.7 * var(--gf-c)) / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 2.4 * var(--gf-c)) calc(100% - 2.4 * var(--gf-c)) no-repeat; + mask: + radial-gradient(var(--gf-c), #000 98%, #0000) 0 0 / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round, + radial-gradient(calc(0.68 * var(--gf-c)), #000 98%, #0000) calc(1.7 * var(--gf-c)) calc(1.7 * var(--gf-c)) / calc(3.4 * var(--gf-c)) calc(3.4 * var(--gf-c)) round, + linear-gradient(#000 0 0) 50% / calc(100% - 2.4 * var(--gf-c)) calc(100% - 2.4 * var(--gf-c)) no-repeat; +} +/* 右上飘着的小云 */ +.gf-cloud::before { + content: ''; + position: absolute; + top: 22px; + right: 24px; + width: 52px; + height: 22px; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 26'%3E%3Cpath d='M13 22a7 7 0 1 1 1.6-13.8A10 10 0 0 1 34 5a8 8 0 0 1 8.6 8A6.5 6.5 0 0 1 41 22z' fill='%23ffffff' fill-opacity='.92'/%3E%3Cellipse cx='54' cy='19' rx='8' ry='4.6' fill='%23ffffff' fill-opacity='.72'/%3E%3C/svg%3E") no-repeat center / contain; + filter: drop-shadow(0 2px 3px rgba(120, 90, 60, 0.14)); + pointer-events: none; + z-index: 1; +} + +/* 晨雾朦胧(四周羽化渐隐 + 底部飘雾,推荐 4:3;无边框无投影更空灵) */ +.gf-mist { + padding: 0; + border: 0; + border-radius: 0; + box-shadow: none; + background: transparent; + -webkit-mask: radial-gradient(112% 112% at 50% 48%, #000 56%, #0000 90%); + mask: radial-gradient(112% 112% at 50% 48%, #000 56%, #0000 90%); +} +/* 底部两团晨雾 */ +.gf-mist::before { + content: ''; + position: absolute; + left: 6%; + right: 6%; + bottom: 4%; + height: 30%; + background: + radial-gradient(58% 100% at 30% 100%, rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0) 70%), + radial-gradient(48% 88% at 72% 100%, rgba(255, 255, 255, 0.38), rgba(255, 255, 255, 0) 72%); + filter: blur(6px); + pointer-events: none; + z-index: 1; +} + +/* 花语卡纸(暖白卡 + 对角花枝,推荐 4:3) */ +.gf-bloom { + padding: 14px; + border: 1px solid rgba(201, 169, 120, 0.4); + border-radius: 10px; + box-shadow: 0 10px 26px rgba(120, 90, 60, 0.14); + background: linear-gradient(160deg, #fffdf8, #fdf7ec); +} +.gf-bloom > img { + border-radius: 5px; +} +/* 左上 / 右下花枝(同一 SVG,右下旋转 180°) */ +.gf-bloom::before, +.gf-bloom::after { + content: ''; + position: absolute; + width: 56px; + height: 56px; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cg fill='none' stroke='%23b8a078' stroke-width='1.4' stroke-linecap='round'%3E%3Cpath d='M6 58C16 46 26 36 46 24'/%3E%3Cpath d='M22 42c7 1 12-2 14-8'/%3E%3Cpath d='M14 50c5 2 10 1 13-3'/%3E%3C/g%3E%3Cg fill='%23a8bfa0'%3E%3Cpath d='M30 33c-1-5 1-8 5-9 1 4-1 7-5 9z'/%3E%3Cpath d='M20 44c-4-2-5-6-3-9 3 2 4 5 3 9z'/%3E%3C/g%3E%3Cg transform='translate(48,20)'%3E%3Cg fill='%23e4aeb6'%3E%3Ccircle cx='0' cy='-5.2' r='3.6'/%3E%3Ccircle cx='4.9' cy='-1.6' r='3.6'/%3E%3Ccircle cx='3.1' cy='4.2' r='3.6'/%3E%3Ccircle cx='-3.1' cy='4.2' r='3.6'/%3E%3Ccircle cx='-4.9' cy='-1.6' r='3.6'/%3E%3C/g%3E%3Ccircle r='2.4' fill='%23c9a24a'/%3E%3C/g%3E%3Cg transform='translate(30,52) scale(.62)'%3E%3Cg fill='%23edc3ca'%3E%3Ccircle cx='0' cy='-5.2' r='3.6'/%3E%3Ccircle cx='4.9' cy='-1.6' r='3.6'/%3E%3Ccircle cx='3.1' cy='4.2' r='3.6'/%3E%3Ccircle cx='-3.1' cy='4.2' r='3.6'/%3E%3Ccircle cx='-4.9' cy='-1.6' r='3.6'/%3E%3C/g%3E%3Ccircle r='2.4' fill='%23c9a24a'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain; + pointer-events: none; + z-index: 1; +} +.gf-bloom::before { + top: 2px; + left: 2px; +} +.gf-bloom::after { + bottom: 2px; + right: 2px; + transform: rotate(180deg); +} + +/* 宽幅全景(16:9 电影画幅白卡 + 云与飞鸟 + 手写落款) */ +.gf-panorama { + padding: 16px 14px 34px; + border: 1px solid rgba(184, 154, 120, 0.3); + border-radius: 4px; + box-shadow: 0 12px 30px rgba(120, 90, 60, 0.16); + background: #fffdf6; +} +/* 顶部白边区:淡云 + 三只飞鸟 */ +.gf-panorama::before { + content: ''; + position: absolute; + top: 3px; + left: 14px; + right: 12px; + height: 12px; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 124 14'%3E%3Cpath d='M8 11a4 4 0 1 1 1-7.9A5.5 5.5 0 0 1 19 2a4.5 4.5 0 0 1 5 4.5A3.8 3.8 0 0 1 22 11z' fill='%23d8c8b0' fill-opacity='.55'/%3E%3Cg fill='none' stroke='%23a08a6a' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M78 8c2.5-2.6 4.5-2.6 7 0M85 8c2.5-2.6 4.5-2.6 7 0'/%3E%3Cpath d='M96 5c2-2.2 3.8-2.2 5.8 0M101.8 5c2-2.2 3.8-2.2 5.8 0'/%3E%3Cpath d='M110 10c1.6-1.8 3-1.8 4.6 0M114.6 10c1.6-1.8 3-1.8 4.6 0'/%3E%3C/g%3E%3C/svg%3E") no-repeat center / contain; + opacity: 0.9; + pointer-events: none; + z-index: 1; +} +/* 底部白边区:手写体落款 */ +.gf-panorama::after { + content: 'with love'; + position: absolute; + left: 0; + right: 0; + bottom: 9px; + text-align: center; + font-family: 'Great Vibes', 'Segoe Script', cursive; + font-size: 15px; + line-height: 1; + color: #b89a78; + letter-spacing: 0.06em; + white-space: nowrap; + overflow: hidden; + pointer-events: none; +} + +/* 横椭圆框(椭圆随 4:3 容器自然拉宽 + 底部花饰小章) */ +.gf-ovalwide { + padding: 7px; + border: 1px solid rgba(201, 169, 120, 0.55); + border-radius: 50%; + box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.7), 0 12px 30px rgba(120, 90, 60, 0.16); + background: #fff; +} +.gf-ovalwide > img { + border-radius: 50%; +} +.gf-ovalwide::before { + content: '❀'; + position: absolute; + bottom: 6px; + left: 50%; + transform: translateX(-50%); + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + line-height: 1; + color: #b08e63; + background: rgba(253, 251, 247, 0.92); + box-shadow: 0 0 0 1px rgba(201, 169, 120, 0.4), 0 2px 5px rgba(120, 90, 60, 0.15); + pointer-events: none; + z-index: 1; +} + +/* 异形裁切类的投影:mask 会裁掉 box-shadow,由父级 drop-shadow 跟随轮廓补投影 */ +.gf-shape-shadow { + filter: drop-shadow(0 10px 18px rgba(96, 72, 50, 0.22)); +} + /* 辅助:图片保持原始宽高比(单图铺满+保比例时使用,覆盖 gf-frame 的 height:100%) */ .gf-natural { height: auto !important; diff --git a/vite-tailwindcss/src/utils/danmakuColors.js b/vite-tailwindcss/src/utils/danmakuColors.js index 8e54c56..422d95d 100644 --- a/vite-tailwindcss/src/utils/danmakuColors.js +++ b/vite-tailwindcss/src/utils/danmakuColors.js @@ -1,6 +1,6 @@ export const DEFAULT_DANMAKU_COLOR = 'champagne' -/** @type {{ key: string, hex: string, label: string, gradient?: string, gradientBg?: string, glow?: string, textLight?: boolean }[]} */ +/** @type {{ key: string, hex: string, label: string, gradient?: string, textGradient?: string, edge?: string }[]} */ export const DANMAKU_COLORS = [ { key: 'champagne', hex: '#a88c6b', label: '香槟金' }, { key: 'blush', hex: '#e8b4b8', label: '浅粉' }, @@ -14,64 +14,55 @@ export const DANMAKU_COLORS = [ ] /** - * 渐变配色: - * - textLight=true → 浅色字(需整体偏深,保证对比度) - * - textLight=false → 深色字(浅色/金色底) + * 渐变配色:渐变用于「文字」(background-clip: text), + * 气泡底统一为通透白纱,避免大色块遮挡照片内容。 + * - gradient : 色点预览 / 文字渐变 + * - textGradient : 文字专用渐变(个别浅色渐变需加深保证白纱底上可读) + * - edge : 气泡描边色(取渐变主色调、低透明度) */ export const DANMAKU_GRADIENT_COLORS = [ { key: 'gradSunset', - hex: '#fff6f2', + hex: '#e11d48', label: '晚霞', - textLight: true, gradient: 'linear-gradient(125deg, #f97316 0%, #e11d48 42%, #c026d3 78%, #7c3aed 100%)', - gradientBg: 'linear-gradient(125deg, rgba(234,88,12,0.94) 0%, rgba(225,29,72,0.93) 42%, rgba(192,38,211,0.93) 78%, rgba(124,58,237,0.94) 100%)', - glow: '0 6px 22px rgba(225,29,72,0.42), 0 2px 8px rgba(124,58,237,0.28)', + edge: 'rgba(225, 29, 72, 0.32)', }, { key: 'gradChampagne', - hex: '#4a3420', + hex: '#a86a24', label: '流金', - textLight: false, gradient: 'linear-gradient(120deg, #fff1c9 0%, #f0c27a 28%, #e8a54b 55%, #c9832f 78%, #8b5a2b 100%)', - gradientBg: 'linear-gradient(120deg, rgba(255,241,201,0.96) 0%, rgba(240,194,122,0.94) 28%, rgba(232,165,75,0.92) 55%, rgba(201,131,47,0.9) 78%, rgba(180,140,80,0.88) 100%)', - glow: '0 6px 20px rgba(201,131,47,0.35), 0 2px 8px rgba(139,90,43,0.22)', + textGradient: 'linear-gradient(120deg, #d9a54a 0%, #c9832f 45%, #a86a24 75%, #8b5a2b 100%)', + edge: 'rgba(201, 131, 47, 0.34)', }, { key: 'gradBlush', - hex: '#fff5fa', + hex: '#e11d74', label: '桃雾', - textLight: true, gradient: 'linear-gradient(130deg, #fb7185 0%, #e11d74 35%, #c026d3 68%, #7c3aed 100%)', - gradientBg: 'linear-gradient(130deg, rgba(244,63,94,0.93) 0%, rgba(219,39,119,0.92) 35%, rgba(192,38,211,0.92) 68%, rgba(124,58,237,0.93) 100%)', - glow: '0 6px 22px rgba(219,39,119,0.4), 0 2px 8px rgba(124,58,237,0.28)', + edge: 'rgba(219, 39, 119, 0.3)', }, { key: 'gradOcean', - hex: '#f0f9ff', + hex: '#0284c7', label: '海暮', - textLight: true, gradient: 'linear-gradient(125deg, #0284c7 0%, #0d9488 32%, #4f46e5 68%, #9333ea 100%)', - gradientBg: 'linear-gradient(125deg, rgba(2,132,199,0.93) 0%, rgba(13,148,136,0.92) 32%, rgba(79,70,229,0.93) 68%, rgba(147,51,234,0.93) 100%)', - glow: '0 6px 22px rgba(2,132,199,0.38), 0 2px 8px rgba(147,51,234,0.26)', + edge: 'rgba(2, 132, 199, 0.3)', }, { key: 'gradAurora', - hex: '#f4fffb', + hex: '#0891b2', label: '极光', - textLight: true, gradient: 'linear-gradient(120deg, #059669 0%, #0891b2 28%, #6366f1 58%, #db2777 100%)', - gradientBg: 'linear-gradient(120deg, rgba(5,150,105,0.93) 0%, rgba(8,145,178,0.92) 28%, rgba(99,102,241,0.93) 58%, rgba(219,39,119,0.93) 100%)', - glow: '0 6px 22px rgba(8,145,178,0.36), 0 2px 10px rgba(219,39,119,0.28)', + edge: 'rgba(8, 145, 178, 0.3)', }, { key: 'gradEmber', - hex: '#fff7ed', + hex: '#dc2626', label: '暖焰', - textLight: true, gradient: 'linear-gradient(125deg, #ea580c 0%, #dc2626 40%, #be123c 70%, #9f1239 100%)', - gradientBg: 'linear-gradient(125deg, rgba(234,88,12,0.94) 0%, rgba(220,38,38,0.93) 40%, rgba(190,18,60,0.93) 70%, rgba(159,18,57,0.94) 100%)', - glow: '0 6px 22px rgba(220,38,38,0.42), 0 2px 8px rgba(190,18,60,0.3)', + edge: 'rgba(220, 38, 38, 0.3)', }, ] @@ -98,20 +89,21 @@ export function resolveDanmakuSwatch(key) { export function resolveDanmakuTint(key) { const entry = BY_KEY[key] || BY_KEY[DEFAULT_DANMAKU_COLOR] - if (entry.gradientBg) { + if (entry.gradient) { + // 渐变字 + 白纱底:彩色只落在文字上,底子通透不挡内容 return { - background: entry.gradientBg, - borderColor: entry.textLight ? 'rgba(255, 255, 255, 0.5)' : 'rgba(74, 52, 32, 0.22)', - boxShadow: entry.glow || '0 6px 20px rgba(0,0,0,0.12)', - textLight: !!entry.textLight, + background: 'rgba(255, 253, 248, 0.4)', + borderColor: entry.edge || 'rgba(168, 140, 107, 0.3)', + boxShadow: '0 2px 10px rgba(88, 68, 42, 0.1)', + textGradient: entry.textGradient || entry.gradient, } } const { r, g, b } = hexToRgb(entry.hex) return { - background: `rgba(${r}, ${g}, ${b}, 0.18)`, - borderColor: `rgba(${r}, ${g}, ${b}, 0.42)`, - boxShadow: '0 4px 14px rgba(88, 68, 42, 0.08)', - textLight: false, + background: `rgba(${r}, ${g}, ${b}, 0.15)`, + borderColor: `rgba(${r}, ${g}, ${b}, 0.38)`, + boxShadow: '0 2px 8px rgba(88, 68, 42, 0.07)', + textGradient: '', } } diff --git a/vite-tailwindcss/src/utils/themes.js b/vite-tailwindcss/src/utils/themes.js new file mode 100644 index 0000000..f1cd362 --- /dev/null +++ b/vite-tailwindcss/src/utils/themes.js @@ -0,0 +1,125 @@ +/** + * 整站风格模板(前后台共用):与 style.css 中 html[data-theme] 变量一一对应 + * - kind: 'pack' 风格包 —— 布局不变,成套更换配色/字体/纹样/装饰 + * - kind: 'layout' 完整模板 —— 首屏封面、章节头、尾页的排版结构也不同 + * - dark: true 暗色主题(night) + */ +export const DEFAULT_SITE_THEME = 'champagne' + +export const SITE_THEMES = [ + // ———— 风格包 ———— + { + key: 'champagne', kind: 'pack', label: '典雅香槟', + desc: '金棕象牙 · 花体英文 · 细金线', + primary: '#a88c6b', line: '#b89a78', bg: '#fbf6ef', + }, + { + key: 'french', kind: 'pack', label: '法式浪漫', + desc: '奶油粉白 · 花体字 · 花卉线描', + primary: '#c78a96', line: '#d9aab4', bg: '#fdf6f4', + }, + { + key: 'modern', kind: 'pack', label: '现代简约', + desc: '暖灰低饱和 · 无衬线 · 细几何线', + primary: '#8a8578', line: '#a39e91', bg: '#f7f6f3', + }, + { + key: 'forest', kind: 'pack', label: '森系自然', + desc: '青绿手写 · 枝叶元素 · 纸纹底', + primary: '#6f9678', line: '#92b399', bg: '#f3f8f2', + }, + { + key: 'night', kind: 'pack', label: '星空夜色', dark: true, + desc: '深蓝烫金 · 暗色主题 · 星月点缀', + primary: '#c9a96a', line: '#8fa3c0', bg: '#101b2e', + }, + // ———— 完整模板 ———— + { + key: 'guofeng-new', kind: 'layout', label: '新中式', + desc: '黛绿朱砂 · 月洞门 · 宋体留白', + primary: '#486156', line: '#b0885a', bg: '#f6f3ec', + }, + { + key: 'guofeng-old', kind: 'layout', label: '老中式', + desc: '朱红鎏金 · 婚书囍字 · 回纹灯彩', + primary: '#a8323e', line: '#c9a24a', bg: '#f9efe6', + }, + { + key: 'magazine', kind: 'layout', label: '杂志封面', + desc: '全幅大图 · 贴边大字 · 编号章节', + primary: '#1f1c19', line: '#8a8378', bg: '#f5f2ec', + }, + { + key: 'newspaper', kind: 'layout', label: '日报编辑', + desc: '报头栏线 · 衬线排印 · 日期栏', + primary: '#2b2822', line: '#6b675e', bg: '#f6f3ea', + }, + { + key: 'pixel', kind: 'layout', label: '复古像素', + desc: '8-bit 像素字 · 游戏界面 · 马赛克心', + primary: '#c94f7c', line: '#5f9ea0', bg: '#faf3e6', + }, + { + key: 'zen', kind: 'layout', label: '极简主义', + desc: '黑白留白 · 细线 · 轴线排版', + primary: '#2a2a28', line: '#8f8d88', bg: '#fafaf8', + }, +] + +export const SITE_THEME_GROUPS = [ + { key: 'pack', label: '风格包 · 换装不换版', items: SITE_THEMES.filter((t) => t.kind === 'pack') }, + { key: 'layout', label: '完整模板 · 排版重构', items: SITE_THEMES.filter((t) => t.kind === 'layout') }, +] + +/** 旧主题值映射(v1 纯色主题 / v2 中式风格包 → 现行风格模板) */ +const LEGACY_THEME_MAP = { + crimson: 'guofeng-old', + chinese: 'guofeng-old', + sage: 'forest', + ocean: 'night', + lilac: 'french', + sunset: 'champagne', +} + +export function normalizeSiteTheme(key) { + if (SITE_THEMES.some((t) => t.key === key)) return key + if (LEGACY_THEME_MAP[key]) return LEGACY_THEME_MAP[key] + return DEFAULT_SITE_THEME +} + +export function siteThemeMeta(key) { + const k = normalizeSiteTheme(key) + return SITE_THEMES.find((t) => t.key === k) +} + +/** 是否完整模板(前台首屏/章节头/尾页走布局分支) */ +export function themeLayoutKind(key) { + return siteThemeMeta(key).kind +} + +/** 是否暗色主题 */ +export function isDarkTheme(key) { + return !!siteThemeMeta(key).dark +} + +/** 应用主题到整页(html[data-theme]),前台按配置、后台跟随编辑值 */ +export function applySiteTheme(key) { + const k = normalizeSiteTheme(key) + document.documentElement.dataset.theme = k + if (k === 'pixel') ensurePixelCjkFont() +} + +/** + * 中文像素字形按需加载: + * 本地 @fontsource 包只含 latin 子集(英文/数字像素),中日韩全量字形 + * 走 unicode-range 分片 CDN(cn-font-split 产物,只下载用到的分片)。 + * 仅在选中 pixel 主题时注入,其他主题零开销。 + */ +function ensurePixelCjkFont() { + if (typeof document === 'undefined' || document.getElementById('pixel-cjk-font')) return + const link = document.createElement('link') + link.id = 'pixel-cjk-font' + link.rel = 'stylesheet' + link.href = 'https://fontsapi.zeoseven.com/570/main/result.css' + document.head.appendChild(link) +} diff --git a/vite-tailwindcss/src/views/admin/AdminEditor.vue b/vite-tailwindcss/src/views/admin/AdminEditor.vue index 97461c3..b2cd574 100644 --- a/vite-tailwindcss/src/views/admin/AdminEditor.vue +++ b/vite-tailwindcss/src/views/admin/AdminEditor.vue @@ -1,14 +1,14 @@ @@ -51,12 +52,15 @@ class="admin-layout-seg" :options="menuLayoutOptions" /> + + {{ livePreviewOn ? '关闭实时预览' : '实时预览' }} + - 预览请柬 + 预览请柬 - 预览海报 + 预览海报 + + 未保存 + 保存配置 @@ -94,6 +107,24 @@
+ + +
@@ -125,8 +157,8 @@
-
访客概览
-
指纹去重访客 · 地区 / 机型画像
+
访客概览
+
指纹去重访客 · 地区 / 机型画像
刷新 @@ -168,7 +200,7 @@ {{ selectedRegion }} · 市级分布 加载中…
- + 关闭
@@ -177,7 +209,7 @@
近期访客预览
- + 查看全部访客记录
@@ -195,7 +227,7 @@ > @@ -217,8 +249,8 @@
-
礼物统计
-
各礼物收到次数 · 合计 {{ giftStats.total }}
+
礼物统计
+
各礼物收到次数 · 合计 {{ giftStats.total }}
刷新 @@ -228,18 +260,18 @@
{{ g.label }}
{{ giftStats.counts[g.key] || 0 }}
-
兑换 {{ giftCostsEdit[g.key] || g.cost }} 赞
+
兑换 {{ giftCostsEdit[g.key] || g.cost }} 赞
-
礼物兑换配置
-
每个礼物消耗的点赞次数 · 默认阶梯 50 / 100 / 200
+
礼物兑换配置
+
每个礼物消耗的点赞次数 · 默认阶梯 50 / 100 / 200
恢复默认 - + 保存配置
@@ -260,8 +292,8 @@
-
礼物明细
-
共 {{ giftList.length }} 条送礼记录
+
礼物明细
+
共 {{ giftList.length }} 条送礼记录
刷新列表 @@ -285,7 +317,7 @@ {{ record.cost || 0 }} 赞