feat: add persistent gift toast and responsive aigc layouts

update aigc page layouts to use viewport units and dynamic sizing instead of hardcoded 812px height for better device compatibility. replace fixed scss values with css custom properties for easier theming and responsive adjustments. refactor templatecarousel component to calculate dynamic card scale and layout based on screen dimensions. add local storage persistence for gift points toast so it stays closed after dismissal. update home page to respect the persisted toast state.
This commit is contained in:
duanshuwen
2026-07-07 09:25:50 +08:00
parent 90352e2981
commit 808ca48668
7 changed files with 135 additions and 36 deletions

View File

@@ -2,6 +2,7 @@ import { currentClientType } from "@/constant/base";
const CLIENT_TYPE = currentClientType(); const CLIENT_TYPE = currentClientType();
const AIGC_CONSENT_AGREED = `${CLIENT_TYPE}_AIGC_CONSENT_AGREED`; const AIGC_CONSENT_AGREED = `${CLIENT_TYPE}_AIGC_CONSENT_AGREED`;
const AIGC_GIFT_POINTS_TOAST_CLOSED = `${CLIENT_TYPE}_AIGC_GIFT_POINTS_TOAST_CLOSED`;
export const getAigcConsentAgreed = () => { export const getAigcConsentAgreed = () => {
return uni.getStorageSync(AIGC_CONSENT_AGREED); return uni.getStorageSync(AIGC_CONSENT_AGREED);
@@ -14,3 +15,15 @@ export const setAigcConsentAgreed = (agreed = true) => {
export const removeAigcConsentAgreed = () => { export const removeAigcConsentAgreed = () => {
return uni.removeStorageSync(AIGC_CONSENT_AGREED); return uni.removeStorageSync(AIGC_CONSENT_AGREED);
}; };
export const getAigcGiftPointsToastClosed = () => {
return uni.getStorageSync(AIGC_GIFT_POINTS_TOAST_CLOSED);
};
export const setAigcGiftPointsToastClosed = (closed = true) => {
return uni.setStorageSync(AIGC_GIFT_POINTS_TOAST_CLOSED, closed);
};
export const removeAigcGiftPointsToastClosed = () => {
return uni.removeStorageSync(AIGC_GIFT_POINTS_TOAST_CLOSED);
};

View File

@@ -1,23 +1,23 @@
.template-card { .template-card {
position: relative; position: relative;
flex: 0 0 318px; flex: 0 0 var(--template-card-width, 318px);
width: 318px; width: var(--template-card-width, 318px);
height: 494px; height: var(--template-card-height, 494px);
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border-radius: 28px; border-radius: var(--template-card-radius, 28px);
background: linear-gradient(180deg, #0f7069, #082226); background: linear-gradient(180deg, #0f7069, #082226);
color: #ffffff; color: #ffffff;
text-align: left; text-align: left;
box-shadow: 0 18px 34px rgba(24, 37, 50, 0.18); box-shadow: 0 3px 8px rgba(24, 37, 50, 0.12);
transform: scale(0.96); transform: scale(0.96);
transform-origin: center; transform-origin: center;
transition: box-shadow 0.18s ease, transform 0.18s ease; transition: box-shadow 0.18s ease, transform 0.18s ease;
} }
.template-card.is-active { .template-card.is-active {
box-shadow: 0 22px 42px rgba(24, 37, 50, 0.2); box-shadow: 0 5px 12px rgba(24, 37, 50, 0.16);
transform: scale(1); transform: scale(1);
} }
@@ -33,8 +33,8 @@
.template-media { .template-media {
position: relative; position: relative;
flex: 0 0 358px; flex: 0 0 var(--template-card-media-height, 358px);
height: 358px; height: var(--template-card-media-height, 358px);
overflow: hidden; overflow: hidden;
background: var(--memory-accent); background: var(--memory-accent);
} }
@@ -58,7 +58,7 @@
.template-copy { .template-copy {
position: relative; position: relative;
flex: 0 0 136px; flex: 0 0 var(--template-card-copy-height, 136px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="template-carousel"> <view class="template-carousel" :style="carouselStyle">
<view class="template-heading"> <view class="template-heading">
<view> <view>
<text class="template-heading-title">选择模板</text> <text class="template-heading-title">选择模板</text>
@@ -42,7 +42,7 @@
</template> </template>
<script setup> <script setup>
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue"; import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import TemplateCard from "../TemplateCard/index.vue"; import TemplateCard from "../TemplateCard/index.vue";
const props = defineProps({ const props = defineProps({
@@ -58,13 +58,28 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue", "select"]); const emit = defineEmits(["update:modelValue", "select"]);
const cardStep = 330; const BASE_CARD_WIDTH = 318;
const cardWidth = 318; const BASE_CARD_HEIGHT = 494;
const sidePadding = 16; const BASE_MEDIA_HEIGHT = 358;
const switchThreshold = 88; const BASE_RAIL_EXTRA_HEIGHT = 18;
const BASE_RAIL_HEIGHT = BASE_CARD_HEIGHT + BASE_RAIL_EXTRA_HEIGHT;
const BASE_TRACK_GAP = 12;
const BASE_TRACK_END_PADDING = 41;
const BASE_SIDE_PADDING = 16;
const BASE_SWITCH_THRESHOLD = 88;
const HEADING_TOTAL_HEIGHT = 54;
const DOTS_TOTAL_HEIGHT = 19;
const FLOW_STEPS_TOTAL_HEIGHT = 67;
const PAGE_BOTTOM_RESERVE = 8;
const MIN_CARD_SCALE = 0.72;
const MAX_CARD_SCALE = 1;
const screenWidth = ref(375); const screenWidth = ref(375);
const screenHeight = ref(812);
const cardScale = ref(1);
const railScrollLeft = ref(0); const railScrollLeft = ref(0);
const currentScrollLeft = ref(0); const currentScrollLeft = ref(0);
const instance = getCurrentInstance();
let touchStartLeft = 0; let touchStartLeft = 0;
let touchStartIndex = 0; let touchStartIndex = 0;
@@ -75,33 +90,102 @@ const getMaxIndex = () => Math.max(props.templates.length - 1, 0);
const clampIndex = (index) => Math.max(0, Math.min(getMaxIndex(), index)); const clampIndex = (index) => Math.max(0, Math.min(getMaxIndex(), index));
const getScreenWidth = () => { const cardWidth = computed(() => Math.round(BASE_CARD_WIDTH * cardScale.value));
const cardHeight = computed(() => Math.round(BASE_CARD_HEIGHT * cardScale.value));
const mediaHeight = computed(() => Math.round(BASE_MEDIA_HEIGHT * cardScale.value));
const copyHeight = computed(() => Math.max(cardHeight.value - mediaHeight.value, 96));
const railHeight = computed(() => Math.round(BASE_RAIL_HEIGHT * cardScale.value));
const trackGap = computed(() => Math.round(BASE_TRACK_GAP * cardScale.value));
const trackEndPadding = computed(() => Math.round(BASE_TRACK_END_PADDING * cardScale.value));
const cardStep = computed(() => cardWidth.value + trackGap.value);
const switchThreshold = computed(() => Math.round(BASE_SWITCH_THRESHOLD * cardScale.value));
const carouselStyle = computed(() => ({
"--template-card-width": `${cardWidth.value}px`,
"--template-card-height": `${cardHeight.value}px`,
"--template-card-media-height": `${mediaHeight.value}px`,
"--template-card-copy-height": `${copyHeight.value}px`,
"--template-card-radius": `${Math.round(28 * cardScale.value)}px`,
"--template-rail-height": `${railHeight.value}px`,
"--template-track-gap": `${trackGap.value}px`,
"--template-track-end-padding": `${trackEndPadding.value}px`,
"--template-side-padding": `${BASE_SIDE_PADDING}px`,
}));
const getScreenSize = () => {
try { try {
const systemInfo = uni.getSystemInfoSync(); const systemInfo = uni.getSystemInfoSync();
return Number(systemInfo.windowWidth) || 375; return {
width: Number(systemInfo.windowWidth) || 375,
height: Number(systemInfo.windowHeight) || 812,
};
} catch (error) { } catch (error) {
return 375; return {
width: 375,
height: 812,
};
} }
}; };
const getCenterOffset = () => (screenWidth.value - cardWidth) / 2 - sidePadding; const getCarouselTop = () =>
new Promise((resolve) => {
try {
let query = uni.createSelectorQuery();
if (instance?.proxy && typeof query.in === "function") {
query = query.in(instance.proxy);
}
query
.select(".template-carousel")
.boundingClientRect((rect) => {
resolve(Number(rect?.top) || 0);
})
.exec();
} catch (error) {
resolve(0);
}
});
const getNextScale = (carouselTop = 0) => {
const availableRailHeight =
screenHeight.value -
carouselTop -
HEADING_TOTAL_HEIGHT -
DOTS_TOTAL_HEIGHT -
FLOW_STEPS_TOTAL_HEIGHT -
PAGE_BOTTOM_RESERVE;
const heightScale = availableRailHeight / BASE_RAIL_HEIGHT;
const widthScale = (screenWidth.value - BASE_SIDE_PADDING * 2) / BASE_CARD_WIDTH;
const nextScale = Math.min(heightScale, widthScale, MAX_CARD_SCALE);
return Math.max(MIN_CARD_SCALE, nextScale);
};
const getCenterOffset = () => (screenWidth.value - cardWidth.value) / 2 - BASE_SIDE_PADDING;
const getTargetScrollLeft = (index) => { const getTargetScrollLeft = (index) => {
const targetIndex = clampIndex(index); const targetIndex = clampIndex(index);
if (targetIndex === 0) return 0; if (targetIndex === 0) return 0;
return Math.round(targetIndex * cardStep - getCenterOffset()); return Math.round(targetIndex * cardStep.value - getCenterOffset());
}; };
const getNearestIndex = (left) => const getNearestIndex = (left) =>
clampIndex(Math.round((left + getCenterOffset()) / cardStep)); clampIndex(Math.round((left + getCenterOffset()) / cardStep.value));
const updateScreenWidth = (width) => { const updateLayoutMetrics = async (size = {}) => {
screenWidth.value = Number(width) || getScreenWidth(); const screenSize = getScreenSize();
screenWidth.value = Number(size.windowWidth) || screenSize.width;
screenHeight.value = Number(size.windowHeight) || screenSize.height;
await nextTick();
const carouselTop = await getCarouselTop();
cardScale.value = getNextScale(carouselTop);
scrollToIndex(props.modelValue); scrollToIndex(props.modelValue);
}; };
const handleWindowResize = (event) => { const handleWindowResize = (event) => {
updateScreenWidth(event?.size?.windowWidth); updateLayoutMetrics(event?.size);
}; };
const setRailScrollLeft = async (targetLeft, force = false) => { const setRailScrollLeft = async (targetLeft, force = false) => {
@@ -159,7 +243,7 @@ const handleTouchEnd = () => {
const offset = currentScrollLeft.value - touchStartLeft; const offset = currentScrollLeft.value - touchStartLeft;
const direction = offset > 0 ? 1 : -1; const direction = offset > 0 ? 1 : -1;
const targetIndex = const targetIndex =
Math.abs(offset) >= switchThreshold Math.abs(offset) >= switchThreshold.value
? touchStartIndex + direction ? touchStartIndex + direction
: touchStartIndex; : touchStartIndex;
@@ -180,13 +264,14 @@ watch(
); );
onMounted(() => { onMounted(() => {
updateScreenWidth(); updateLayoutMetrics();
if (typeof uni.onWindowResize === "function") { if (typeof uni.onWindowResize === "function") {
uni.onWindowResize(handleWindowResize); uni.onWindowResize(handleWindowResize);
} }
}); });
onUnmounted(() => { onUnmounted(() => {
clearTimeout(settleTimer);
if (typeof uni.offWindowResize === "function") { if (typeof uni.offWindowResize === "function") {
uni.offWindowResize(handleWindowResize); uni.offWindowResize(handleWindowResize);
} }

View File

@@ -46,7 +46,7 @@
.template-rail { .template-rail {
max-width: 100vw; max-width: 100vw;
height: 560px; height: var(--template-rail-height, 560px);
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
scrollbar-width: none; scrollbar-width: none;
@@ -59,9 +59,9 @@
.template-track { .template-track {
display: inline-flex; display: inline-flex;
align-items: flex-start; align-items: flex-start;
gap: 12px; gap: var(--template-track-gap, 12px);
height: 560px; height: var(--template-rail-height, 560px);
padding: 0 41px 0 16px; padding: 0 var(--template-track-end-padding, 41px) 0 var(--template-side-padding, 16px);
box-sizing: border-box; box-sizing: border-box;
} }

View File

@@ -15,6 +15,7 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue"; import AigcTopBar from "@/pages-aigc/components/AigcTopBar/index.vue";
import { getAigcGiftPointsToastClosed, setAigcGiftPointsToastClosed } from "@/constant/aigc.js";
import GiftPointsToast from "./components/GiftPointsToast/index.vue"; import GiftPointsToast from "./components/GiftPointsToast/index.vue";
import TemplateCarousel from "./components/TemplateCarousel/index.vue"; import TemplateCarousel from "./components/TemplateCarousel/index.vue";
import FlowSteps from "./components/FlowSteps/index.vue"; import FlowSteps from "./components/FlowSteps/index.vue";
@@ -22,7 +23,7 @@ import { aigcFlowSteps, aigcTemplates } from "./data/templates.js";
const pointBalance = ref(300); const pointBalance = ref(300);
const activeIndex = ref(0); const activeIndex = ref(0);
const giftToastVisible = ref(true); const giftToastVisible = ref(!getAigcGiftPointsToastClosed());
const templates = aigcTemplates; const templates = aigcTemplates;
const steps = aigcFlowSteps; const steps = aigcFlowSteps;
@@ -47,6 +48,7 @@ const handleRecharge = () => {
const handleCloseGiftToast = () => { const handleCloseGiftToast = () => {
giftToastVisible.value = false; giftToastVisible.value = false;
setAigcGiftPointsToastClosed();
}; };
const handleSelectTemplate = (template) => { const handleSelectTemplate = (template) => {

View File

@@ -2,8 +2,8 @@
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 100vw; width: 100vw;
height: 812px; height: 100vh;
overflow: hidden; overflow: hidden;
color: #172033; color: #172033;
background: background:
@@ -18,7 +18,7 @@
.aigc-home-browser { .aigc-home-browser {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 0;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
padding: 4px 0 0; padding: 4px 0 0;

View File

@@ -4,7 +4,6 @@
flex-direction: column; flex-direction: column;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
min-height: 812px;
overflow: hidden; overflow: hidden;
color: #172033; color: #172033;
background: background:
@@ -19,7 +18,7 @@
.aigc-use-template-body { .aigc-use-template-body {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 0;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} }