From fe7fbba1fc142a33f079bd660f9f9d6ef8c1b121 Mon Sep 17 00:00:00 2001
From: liulong <18539103286>
Date: Thu, 14 May 2026 16:42:27 +0800
Subject: [PATCH] =?UTF-8?q?chore:=20=E5=BD=92=E6=A1=A3=E5=BD=93=E5=89=8D?=
=?UTF-8?q?=E7=8E=B0=E6=9C=89=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=87=86=E5=A4=87?=
=?UTF-8?q?=E6=96=B0=E5=BB=BA=E5=88=86=E6=94=AF=E5=BC=80=E5=8F=91=E6=8F=92?=
=?UTF-8?q?=E4=BB=B6=E9=AA=8C=E8=AF=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../sql/migrations/V5__mint_cost_config.sql | 53 +
frontend/components/ConfirmModal.vue | 10 +-
.../lenticular/CastloveLenticularPreview.vue | 55 +
.../components/lenticular/LenticularCard.vue | 352 ++
frontend/composables/useLenticularPreview.js | 155 +
frontend/pages.json | 19 +
frontend/pages/castlove/craft-select.vue | 67 +-
frontend/pages/castlove/create.vue | 860 ++++-
frontend/pages/castlove/laser-card-studio.vue | 3336 +++++++++++++++++
frontend/pages/castlove/lenticular-studio.vue | 657 ++++
.../pages/components/HorizontalScroll.vue | 8 +-
frontend/pages/discover/discover.vue | 31 +-
frontend/pages/discover/generation-result.vue | 36 +-
frontend/utils/api.js | 112 +-
frontend/utils/castloveAfterLaserMint.js | 179 +
frontend/utils/castloveMintForm.js | 138 +
frontend/utils/h5OssPostUrl.js | 26 +
.../utils/laser-card/aliyunPortraitUni.js | 557 +++
frontend/utils/laser-card/segmentApi.js | 48 +
.../utils/laser-card/segmentationCloud.js | 320 ++
frontend/utils/lenticular-engine.js | 256 ++
frontend/vite.config.js | 83 +
22 files changed, 7120 insertions(+), 238 deletions(-)
create mode 100644 docker/sql/migrations/V5__mint_cost_config.sql
create mode 100644 frontend/components/lenticular/CastloveLenticularPreview.vue
create mode 100644 frontend/components/lenticular/LenticularCard.vue
create mode 100644 frontend/composables/useLenticularPreview.js
create mode 100644 frontend/pages/castlove/laser-card-studio.vue
create mode 100644 frontend/pages/castlove/lenticular-studio.vue
create mode 100644 frontend/utils/castloveAfterLaserMint.js
create mode 100644 frontend/utils/castloveMintForm.js
create mode 100644 frontend/utils/h5OssPostUrl.js
create mode 100644 frontend/utils/laser-card/aliyunPortraitUni.js
create mode 100644 frontend/utils/laser-card/segmentApi.js
create mode 100644 frontend/utils/laser-card/segmentationCloud.js
create mode 100644 frontend/utils/lenticular-engine.js
create mode 100644 frontend/vite.config.js
diff --git a/docker/sql/migrations/V5__mint_cost_config.sql b/docker/sql/migrations/V5__mint_cost_config.sql
new file mode 100644
index 0000000..7c98ac5
--- /dev/null
+++ b/docker/sql/migrations/V5__mint_cost_config.sql
@@ -0,0 +1,53 @@
+-- 铸造经济:按「第几次铸爱」阶梯扣水晶(见 docs/specs/2026-04-15-economic-system-design.md)
+-- 未建表/未插数时,资产服务查配置会得到 record not found,网关表现为 code 17。
+
+-- 用户在某 star 下的累计铸爱次数(CreateMintOrder 会读/写)
+CREATE TABLE IF NOT EXISTS public.user_mint_count (
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL,
+ star_id BIGINT NOT NULL,
+ mint_count INT NOT NULL DEFAULT 0,
+ revenue_boost_bps INT NOT NULL DEFAULT 0,
+ updated_at BIGINT NOT NULL,
+ CONSTRAINT uk_user_mint_count_user_star UNIQUE (user_id, star_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_user_mint_count_user ON public.user_mint_count USING btree (user_id);
+CREATE INDEX IF NOT EXISTS idx_user_mint_count_star ON public.user_mint_count USING btree (star_id);
+
+COMMENT ON TABLE public.user_mint_count IS '用户在某偶像下的累计铸爱次数与收益加成(基点)';
+
+-- 第 N 次铸爱对应扣费与保底概率(mint_count 唯一)
+CREATE TABLE IF NOT EXISTS public.mint_cost_config (
+ id BIGSERIAL PRIMARY KEY,
+ mint_count INT NOT NULL,
+ cost_crystal BIGINT NOT NULL,
+ probability BIGINT NOT NULL DEFAULT 0,
+ reward_type VARCHAR(50) DEFAULT NULL,
+ reward_value BIGINT NOT NULL DEFAULT 0,
+ description VARCHAR(255),
+ updated_at BIGINT NOT NULL,
+ CONSTRAINT uk_mint_cost_config_mint_count UNIQUE (mint_count)
+);
+
+COMMENT ON TABLE public.mint_cost_config IS '铸爱次数阶梯消耗与保底配置';
+
+INSERT INTO public.mint_cost_config (mint_count, cost_crystal, probability, reward_type, reward_value, description, updated_at)
+VALUES
+ (1, 2, 0, NULL, 0, '第1次', 1773322573872),
+ (2, 4, 0, NULL, 0, '第2次', 1773322573872),
+ (3, 8, 0, NULL, 0, '第3次', 1773322573872),
+ (4, 16, 0, NULL, 0, '第4次', 1773322573872),
+ (5, 32, 0, NULL, 0, '第5次', 1773322573872),
+ (6, 64, 0, NULL, 0, '第6次', 1773322573872),
+ (7, 128, 0, NULL, 0, '第7次', 1773322573872),
+ (8, 256, 0, NULL, 0, '第8次', 1773322573872),
+ (9, 512, 20, '收益提升', 5, '小保底', 1773322573872),
+ (10, 1024, 100, '收益提升', 5, '大保底', 1773322573872)
+ON CONFLICT (mint_count) DO UPDATE SET
+ cost_crystal = EXCLUDED.cost_crystal,
+ probability = EXCLUDED.probability,
+ reward_type = EXCLUDED.reward_type,
+ reward_value = EXCLUDED.reward_value,
+ description = EXCLUDED.description,
+ updated_at = EXCLUDED.updated_at;
diff --git a/frontend/components/ConfirmModal.vue b/frontend/components/ConfirmModal.vue
index da309a6..1fb90e5 100644
--- a/frontend/components/ConfirmModal.vue
+++ b/frontend/components/ConfirmModal.vue
@@ -24,11 +24,13 @@
-
+
+
+
diff --git a/frontend/components/lenticular/LenticularCard.vue b/frontend/components/lenticular/LenticularCard.vue
new file mode 100644
index 0000000..9ee5c85
--- /dev/null
+++ b/frontend/components/lenticular/LenticularCard.vue
@@ -0,0 +1,352 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ↻
+ {{ tiltHintText }}
+ 叠化近似预览(倾斜或拖动体验光栅效果)
+
+
+
+
+
+
+
+
diff --git a/frontend/composables/useLenticularPreview.js b/frontend/composables/useLenticularPreview.js
new file mode 100644
index 0000000..69375a8
--- /dev/null
+++ b/frontend/composables/useLenticularPreview.js
@@ -0,0 +1,155 @@
+/**
+ * 光栅卡预览:触摸模拟倾斜 + LenticularEngine(不启动硬件传感器)
+ * @param {import('vue').Ref} layersRef 图层配置(reactive/ref 均可被 .value 读取时用 ref)
+ */
+import { reactive, ref, watch, onMounted, onUnmounted } from 'vue'
+import { LenticularEngine, DEFAULT_PHYSICS } from '@/utils/lenticular-engine.js'
+
+function clamp(v, min, max) {
+ return Math.max(min, Math.min(max, v))
+}
+
+export function useLenticularPreview(layersRef) {
+ const physics = reactive({ ...DEFAULT_PHYSICS })
+ physics.gyroSimEnabled = false
+
+ const sensorData = ref({ gamma: 0, beta: 0, timestamp: Date.now() })
+ const source = ref('simulation')
+
+ function simulate(x, _y) {
+ sensorData.value = {
+ gamma: clamp(x, -1, 1),
+ beta: 0,
+ timestamp: Date.now(),
+ }
+ }
+
+ function relax(factor = 0.85) {
+ sensorData.value = {
+ gamma: sensorData.value.gamma * factor,
+ beta: 0,
+ timestamp: Date.now(),
+ }
+ }
+
+ const engine = new LenticularEngine(physics)
+ const initial = layersRef.value || layersRef
+ engine.setLayers(Array.isArray(initial) ? initial : [])
+
+ const layerTransforms = ref({})
+
+ function rebuildTransformsFromLayers(ls) {
+ const prev = layerTransforms.value
+ const next = {}
+ for (const l of ls) {
+ const p = prev[l.id]
+ next[l.id] =
+ p != null ? p : { x: 0, y: 0, opacity: l.opacity }
+ }
+ layerTransforms.value = next
+ }
+
+ rebuildTransformsFromLayers(engine.layers.length ? engine.layers : layersRef.value || [])
+
+ const stripeRender = ref({
+ phaseShift: 0,
+ shares: [1 / 3, 1 / 3, 1 / 3],
+ pitchPx: DEFAULT_PHYSICS.lenticularPitchPx,
+ prevLayerGhost: null,
+ })
+
+ let rafId = null
+ const nextFrame =
+ typeof requestAnimationFrame === 'function'
+ ? (cb) => requestAnimationFrame(cb)
+ : (cb) => setTimeout(cb, 16)
+ const cancelFrame =
+ typeof cancelAnimationFrame === 'function'
+ ? (id) => cancelAnimationFrame(id)
+ : (id) => clearTimeout(id)
+
+ function getLayersArray() {
+ const v = layersRef.value !== undefined ? layersRef.value : layersRef
+ return Array.isArray(v) ? v : []
+ }
+
+ function tick() {
+ try {
+ const ls = getLayersArray()
+ const renderState = engine.feedSimulatedTilt(sensorData.value.gamma, sensorData.value.beta)
+ const next = {}
+ for (const layer of ls) {
+ const offset = renderState.layerOffsets.get(layer.id)
+ const opacity = renderState.layerOpacities.get(layer.id)
+ next[layer.id] = {
+ x: offset != null ? offset.x : 0,
+ y: offset != null ? offset.y : 0,
+ opacity: opacity != null ? opacity : layer.opacity,
+ }
+ }
+ layerTransforms.value = next
+ stripeRender.value = {
+ phaseShift: renderState.stripePhaseShift,
+ shares: [...renderState.stripShares],
+ pitchPx: renderState.lenticularPitchPx,
+ prevLayerGhost: renderState.prevLayerGhost,
+ }
+ } catch (e) {
+ console.error('[useLenticularPreview] tick failed', e)
+ }
+ rafId = nextFrame(tick)
+ }
+
+ function startRenderLoop() {
+ if (rafId != null) return
+ rafId = nextFrame(tick)
+ }
+
+ function stopRenderLoop() {
+ if (rafId != null) {
+ cancelFrame(rafId)
+ rafId = null
+ }
+ }
+
+ watch(
+ layersRef,
+ (ls) => {
+ const arr = Array.isArray(ls) ? ls : []
+ engine.setLayers(arr)
+ rebuildTransformsFromLayers(arr)
+ },
+ { deep: true, immediate: true }
+ )
+
+ onMounted(() => {
+ startRenderLoop()
+ })
+
+ onUnmounted(() => {
+ stopRenderLoop()
+ })
+
+ const gyro = {
+ sensorData,
+ source,
+ simulate,
+ relax,
+ start: () => {},
+ stop: () => {
+ source.value = 'simulation'
+ },
+ }
+
+ return {
+ physics,
+ layerTransforms,
+ stripeRender,
+ gyro,
+ simulate,
+ relax,
+ engine,
+ startRenderLoop,
+ stopRenderLoop,
+ }
+}
diff --git a/frontend/pages.json b/frontend/pages.json
index a41389f..3600c4a 100644
--- a/frontend/pages.json
+++ b/frontend/pages.json
@@ -134,6 +134,25 @@
}
}
},
+ {
+ "path": "pages/castlove/lenticular-studio",
+ "style": {
+ "navigationStyle": "custom",
+ "app-plus": {
+ "bounce": "none"
+ }
+ }
+ },
+ {
+ "path": "pages/castlove/laser-card-studio",
+ "style": {
+ "navigationStyle": "custom",
+ "backgroundColor": "#0a0a0a",
+ "app-plus": {
+ "bounce": "none"
+ }
+ }
+ },
{
"path": "pages/castlove/craft-select",
"style": {
diff --git a/frontend/pages/castlove/craft-select.vue b/frontend/pages/castlove/craft-select.vue
index c62b9f8..a1948e3 100644
--- a/frontend/pages/castlove/craft-select.vue
+++ b/frontend/pages/castlove/craft-select.vue
@@ -5,9 +5,12 @@
-
+ :class="{ 'card-selected': selectedIndex === index }" :style="getCardStyle(index)">
+
@@ -48,6 +51,18 @@
+
@@ -802,6 +1001,24 @@ onMounted(() => {
text-align: center;
}
+.lenticular-upload-row {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: stretch;
+ gap: 20rpx;
+ padding: 0 4rpx;
+ box-sizing: border-box;
+}
+
+.upload-box--half {
+ flex: 1;
+ width: 0;
+ min-width: 0;
+ height: 280rpx;
+}
+
.uploaded-image {
width: 100%;
height: 100%;
@@ -1156,6 +1373,301 @@ onMounted(() => {
opacity: 0.9;
}
+/* ========== 镭射卡设计稿专用 ========== */
+.page-container--laser {
+ position: relative;
+}
+
+.laser-bg-veil {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(2rpx 2rpx at 10% 18%, rgba(255, 255, 255, 0.55), transparent 55%),
+ radial-gradient(1.5rpx 1.5rpx at 78% 12%, rgba(255, 255, 255, 0.45), transparent 55%),
+ radial-gradient(2rpx 2rpx at 42% 8%, rgba(255, 255, 255, 0.4), transparent 50%),
+ radial-gradient(1.5rpx 1.5rpx at 88% 46%, rgba(255, 255, 255, 0.5), transparent 50%),
+ radial-gradient(2rpx 2rpx at 18% 62%, rgba(255, 255, 255, 0.35), transparent 50%),
+ radial-gradient(1.5rpx 1.5rpx at 64% 72%, rgba(255, 255, 255, 0.45), transparent 50%),
+ radial-gradient(2rpx 2rpx at 36% 88%, rgba(255, 255, 255, 0.4), transparent 50%),
+ radial-gradient(1.5rpx 1.5rpx at 92% 84%, rgba(255, 255, 255, 0.35), transparent 50%),
+ linear-gradient(
+ 165deg,
+ rgba(255, 182, 220, 0.16) 0%,
+ rgba(140, 100, 210, 0.22) 42%,
+ rgba(20, 40, 80, 0.2) 100%
+ );
+}
+
+.laser-close-hit {
+ position: fixed;
+ top: calc(16rpx + constant(safe-area-inset-top));
+ top: calc(16rpx + env(safe-area-inset-top));
+ left: 24rpx;
+ z-index: 20;
+ width: 72rpx;
+ height: 72rpx;
+ border-radius: 50%;
+ background: linear-gradient(
+ 135deg,
+ #ffb7e8 0%,
+ #c9a6ff 28%,
+ #8fd4ff 55%,
+ #fff3b0 78%,
+ #ffa8d8 100%
+ );
+ background-size: 180% 180%;
+ border: 2rpx solid rgba(255, 255, 255, 0.55);
+ box-shadow: 0 6rpx 22rpx rgba(120, 60, 160, 0.35);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.laser-close-x {
+ color: #ffffff;
+ font-size: 44rpx;
+ line-height: 1;
+ font-weight: 300;
+}
+
+.content-wrapper--laser {
+ padding-top: calc(112rpx + constant(safe-area-inset-top));
+ padding-top: calc(112rpx + env(safe-area-inset-top));
+}
+
+.upload-section--laser .upload-box-wrap {
+ position: relative;
+ width: 100%;
+ max-width: 640rpx;
+ margin: 0 auto;
+}
+
+.upload-section--laser .upload-box--laser {
+ position: relative;
+ width: 100%;
+ height: 460rpx;
+ border-radius: 36rpx;
+ border: 2rpx solid rgba(255, 255, 255, 0.32);
+ box-shadow: 0 18rpx 56rpx rgba(60, 30, 100, 0.28);
+ background: rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(18rpx);
+ -webkit-backdrop-filter: blur(18rpx);
+ /* 外框实线;内层虚线框见 .upload-laser-dashed-inner */
+ border-style: solid;
+}
+
+.upload-section--laser .upload-box--laser .uploaded-image {
+ border-radius: 32rpx;
+}
+
+.upload-laser-empty {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-sizing: border-box;
+ padding: 28rpx;
+}
+
+.upload-laser-watermark {
+ position: absolute;
+ left: 0;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ z-index: 0;
+ opacity: 0.35;
+ pointer-events: none;
+ background:
+ radial-gradient(ellipse 52% 78% at 50% 100%, rgba(60, 35, 90, 0.55), transparent 58%),
+ radial-gradient(ellipse 42% 55% at 50% 36%, rgba(140, 110, 180, 0.45), transparent 58%),
+ radial-gradient(ellipse 28% 22% at 50% 16%, rgba(200, 180, 230, 0.35), transparent 70%),
+ linear-gradient(185deg, rgba(255, 210, 240, 0.15) 0%, rgba(80, 50, 120, 0.18) 100%);
+}
+
+.upload-laser-dashed-inner {
+ position: relative;
+ z-index: 1;
+ width: 82%;
+ height: 78%;
+ max-width: 520rpx;
+ max-height: 360rpx;
+ border: 3rpx dashed rgba(255, 255, 255, 0.92);
+ border-radius: 28rpx;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 20rpx;
+ background: rgba(255, 255, 255, 0.04);
+}
+
+.upload-plus-ring {
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 50%;
+ border: 3rpx solid rgba(255, 255, 255, 0.95);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 8rpx 28rpx rgba(40, 20, 80, 0.2);
+}
+
+.upload-plus--laser {
+ font-size: 64rpx;
+ line-height: 1;
+ color: rgba(255, 255, 255, 0.98);
+ font-weight: 200;
+}
+
+.upload-text--laser {
+ font-size: 26rpx;
+ color: rgba(255, 255, 255, 0.95);
+ font-weight: 500;
+}
+
+.upload-section--laser .upload-hint {
+ color: rgba(255, 255, 255, 0.88);
+ font-size: 22rpx;
+ margin-top: 16rpx;
+}
+
+.upload-section--laser .upload-clear {
+ position: absolute;
+ top: 14rpx;
+ right: 14rpx;
+ z-index: 6;
+ width: 56rpx;
+ height: 56rpx;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.upload-clear--laser {
+ background: rgba(255, 255, 255, 0.96);
+ border: 2rpx solid rgba(255, 255, 255, 0.95);
+ box-shadow: 0 6rpx 18rpx rgba(60, 30, 100, 0.22);
+}
+
+.upload-clear-x--laser {
+ color: rgba(90, 70, 120, 0.85);
+ font-size: 36rpx;
+ font-weight: 300;
+}
+
+.form-section--laser {
+ gap: 36rpx;
+}
+
+.form-label--laser,
+.section-title--laser {
+ font-size: 28rpx;
+ padding: 10rpx 22rpx;
+ border-radius: 999rpx;
+ background: linear-gradient(95deg, #ff7ec8 0%, #ff9f7a 48%, #ffb35c 100%);
+ color: #ffffff;
+ text-shadow: 0 2rpx 6rpx rgba(160, 40, 80, 0.35);
+ box-shadow: 0 4rpx 14rpx rgba(255, 100, 140, 0.28);
+}
+
+.ai-section--laser {
+ gap: 20rpx;
+}
+
+.picker-display--laser {
+ background: rgba(255, 255, 255, 0.32);
+ backdrop-filter: blur(22rpx);
+ -webkit-backdrop-filter: blur(22rpx);
+ border: 2rpx solid rgba(255, 255, 255, 0.45);
+ box-shadow: 0 8rpx 28rpx rgba(50, 20, 90, 0.12);
+}
+
+.form-textarea--laser {
+ background: rgba(255, 255, 255, 0.32);
+ backdrop-filter: blur(22rpx);
+ -webkit-backdrop-filter: blur(22rpx);
+ border: 2rpx solid rgba(255, 255, 255, 0.42);
+ box-shadow: 0 8rpx 28rpx rgba(50, 20, 90, 0.1);
+}
+
+.picker-arrow.picker-arrow--laser {
+ transform: rotate(0deg);
+ font-size: 22rpx;
+ line-height: 1;
+ color: rgba(255, 255, 255, 0.88);
+ font-weight: 600;
+}
+
+.picker-arrow.picker-arrow--laser.picker-arrow-up {
+ transform: rotate(180deg);
+}
+
+.ai-input-wrapper--laser {
+ flex-direction: column;
+ min-height: 280rpx;
+ padding: 24rpx 28rpx 28rpx;
+ background: rgba(255, 255, 255, 0.32);
+ backdrop-filter: blur(22rpx);
+ -webkit-backdrop-filter: blur(22rpx);
+ border: 2rpx solid rgba(255, 255, 255, 0.45);
+ box-shadow: 0 8rpx 28rpx rgba(50, 20, 90, 0.1);
+}
+
+.ai-input-wrapper--laser .ai-input {
+ min-height: 220rpx;
+ width: 100%;
+}
+
+.button-section--laser {
+ gap: 24rpx;
+ margin-top: 48rpx;
+}
+
+.btn-secondary--laser {
+ background: rgba(255, 255, 255, 0.18);
+ border-color: rgba(255, 255, 255, 0.35);
+}
+
+.btn-confirm-laser {
+ flex: 1;
+ height: 96rpx;
+ line-height: 96rpx;
+ border-radius: 48rpx;
+ font-size: 36rpx;
+ font-family: 'yt', sans-serif;
+ font-weight: 600;
+ color: #ffffff;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: linear-gradient(95deg, #ff8ec5 0%, #ff5a8c 38%, #ff2d6b 72%, #ff4d64 100%);
+ box-shadow: 0 10rpx 32rpx rgba(255, 50, 110, 0.42);
+ text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.25);
+}
+
+.btn-confirm-laser::after {
+ border: none;
+}
+
+.btn-confirm-laser:active {
+ opacity: 0.92;
+}
+
.nav-mask {
position: fixed;
top: 0;
diff --git a/frontend/pages/castlove/laser-card-studio.vue b/frontend/pages/castlove/laser-card-studio.vue
new file mode 100644
index 0000000..8ecfcc5
--- /dev/null
+++ b/frontend/pages/castlove/laser-card-studio.vue
@@ -0,0 +1,3336 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 换图
+
+
+ {{ previewHintText }}
+
+
+
+
+ +
+
+ 拍照或上传
+ 支持相册选择或相机拍摄
+
+
+
+
+
+
+ 我的预设
+ 点击套用 · 长按删除
+
+
+ {{ p.name }}
+ {{ presetChipMeta(p) }}
+
+
+
+
+
+
+ 当前效果
+
+
+ 风格
+ {{ currentPreset.name }}
+
+
+ 调校
+ {{ tuneSummary }}
+
+
+
+
+
+
+
+ {{ activePanelMeta.title }}
+ {{ activePanelMeta.desc }}
+
+
+ {{ activePanelMeta.tag }}
+
+
+
+
+
+
+
+
+ {{ p.name }}
+ {{ p.desc }}
+
+
+
+
+
+
+
+
+ 镭射强度
+ {{ laserStrength }}%
+
+ 整体幻彩与光束的强弱,人像建议约 45%–65%
+
+
+ 淡
+ 浓
+
+
+ 流体底纹
+ {{ marbleFlowStrength }}%
+
+ 叠加大理石/揉箔感的中频纹理,亮带更随型(略增绘制开销)
+
+
+
+
+
+ 光柱强度
+ {{ beamIntensity }}%
+
+ 斜射光柱的亮度与宽度,值越高光柱越粗越耀眼
+
+
+ 柔
+ 烈
+
+
+
+
+
+ 智能抠图
+ 云端
+
+ 方案一:在 config/segmentApi.js 配置 ALIYUN_STS_URL + OSS_BUCKET(STS 仅签发临时密钥,不写 AK)。方案二:自建代理 SEGMENT_API_BASE。云端方案为稳定读图,上传前不再本地压缩(体积略大)。
+
+
+ 智能抠图
+
+
+ 恢复椭圆
+
+
+
+
+
+
+ 人物范围
+ 椭圆抠图
+
+ 用椭圆把人物从背景中抠出,置于镭射光效之上(拖动预览定位中心;椭圆为近似形状,背景过于杂乱时边缘会可见){{ (segmentationMode === 'ai' && aiCutoutSrc) ? ' · 当前为智能抠图,点上方「恢复椭圆」后可调下列滑块。' : '' }}
+
+ 主体横向范围
+ {{ subjectMaskRadiusX }}%
+
+
+
+ 主体纵向范围
+ {{ subjectMaskRadiusY }}%
+
+
+
+ 边缘柔和
+ {{ subjectMaskSoftness }}%
+
+
+
+ 人物区压镭射
+ {{ subjectLaserSuppress }}%
+
+ 只作用于人物图下方的幻彩:压低边缘透闪,不会让人脸变亮(与「人像膜层」分开调)
+
+
+ 人像膜层幻彩
+ {{ portraitVeilStrength }}%
+
+ 叠在照片最上层(soft-light),模拟参考卡里人物区域仍有光油/薄膜幻彩;想接近参考图请调高此项
+
+
+ 磨砂强化
+ {{ matteStrength }}%
+
+ 叠加雾面颗粒与微纹理,更接近实体磨砂卡(会抬高闪粉有效密度)
+
+
+
+
+
+ 光束角度
+ {{ angle }}°
+
+ 绕预览区拖定的枢轴旋转;下方可一键常用角度,再滑杆微调
+
+
+ 0°
+ 360°
+
+
+
+ {{ d }}°
+
+
+
+
+ −15°
+
+
+ −5°
+
+
+ +5°
+
+
+ +15°
+
+
+
+
+
+
+ 光束光效
+ {{ currentBeamEffect.name }}
+
+ 主光束与扫光可切换炫彩,不仅限于白光
+
+
+
+ {{ b.name }}
+
+
+
+
+
+
+ 彩虹细纹
+ {{ diffractionDensity }}%
+
+ 满铺斜向彩虹条纹的密度,越高条纹越细越密
+
+
+ 稀
+ 密
+
+
+ 条纹角度偏移
+ {{ diffractionAngleOffset }}°
+
+ 相对光束角度的偏移,90° 表示与光线方向垂直(最像衍射纹)
+
+
+
+
+
+ 闪粉细腻度
+ {{ grainDensity }}%
+
+ 越高颗粒越密;开启「磨砂强化」后等效细腻度约 {{ grainDensityEffective }}%
+
+
+ 粗
+ 细
+
+
+
+ 展示预览(可选)
+
+
+ 动效速度
+ {{ previewAnimSpeed }}%
+
+ 现场给粉丝看时可略加快
+
+
+ 慢
+ 快
+
+
+
+
+
+ 色彩鲜艳度
+ {{ previewVivid }}%
+
+ 控制预览里彩虹/偏色的饱和度感
+
+
+ 淡
+ 艳
+
+
+
+
+
+
+
+
+
+ {{ t.icon }}
+ {{ t.label }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/castlove/lenticular-studio.vue b/frontend/pages/castlove/lenticular-studio.vue
new file mode 100644
index 0000000..eabfb80
--- /dev/null
+++ b/frontend/pages/castlove/lenticular-studio.vue
@@ -0,0 +1,657 @@
+
+
+
+
+ ←
+
+
+ 光栅卡工作室
+ 倾斜手机 · 重力感应预览
+
+
+ ⋮
+
+
+
+
+
+
+
+
+
+ 校零(水平对准)
+
+ {{ gyroHintLine }}
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ {{ layer.label }}
+ {{ layer.src ? '已上传' : '点击上传' }}
+
+
+ ×
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/pages/components/HorizontalScroll.vue b/frontend/pages/components/HorizontalScroll.vue
index d60eed8..369d696 100644
--- a/frontend/pages/components/HorizontalScroll.vue
+++ b/frontend/pages/components/HorizontalScroll.vue
@@ -12,10 +12,10 @@
-