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 AIGC_CONSENT_AGREED = `${CLIENT_TYPE}_AIGC_CONSENT_AGREED`;
const AIGC_GIFT_POINTS_TOAST_CLOSED = `${CLIENT_TYPE}_AIGC_GIFT_POINTS_TOAST_CLOSED`;
export const getAigcConsentAgreed = () => {
return uni.getStorageSync(AIGC_CONSENT_AGREED);
@@ -14,3 +15,15 @@ export const setAigcConsentAgreed = (agreed = true) => {
export const removeAigcConsentAgreed = () => {
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 {
position: relative;
flex: 0 0 318px;
width: 318px;
height: 494px;
flex: 0 0 var(--template-card-width, 318px);
width: var(--template-card-width, 318px);
height: var(--template-card-height, 494px);
overflow: hidden;
display: flex;
flex-direction: column;
border-radius: 28px;
border-radius: var(--template-card-radius, 28px);
background: linear-gradient(180deg, #0f7069, #082226);
color: #ffffff;
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-origin: center;
transition: box-shadow 0.18s ease, transform 0.18s ease;
}
.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);
}
@@ -33,8 +33,8 @@
.template-media {
position: relative;
flex: 0 0 358px;
height: 358px;
flex: 0 0 var(--template-card-media-height, 358px);
height: var(--template-card-media-height, 358px);
overflow: hidden;
background: var(--memory-accent);
}
@@ -58,7 +58,7 @@
.template-copy {
position: relative;
flex: 0 0 136px;
flex: 0 0 var(--template-card-copy-height, 136px);
display: flex;
flex-direction: column;
justify-content: flex-start;

View File

@@ -1,5 +1,5 @@
<template>
<view class="template-carousel">
<view class="template-carousel" :style="carouselStyle">
<view class="template-heading">
<view>
<text class="template-heading-title">选择模板</text>
@@ -42,7 +42,7 @@
</template>
<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";
const props = defineProps({
@@ -58,13 +58,28 @@ const props = defineProps({
const emit = defineEmits(["update:modelValue", "select"]);
const cardStep = 330;
const cardWidth = 318;
const sidePadding = 16;
const switchThreshold = 88;
const BASE_CARD_WIDTH = 318;
const BASE_CARD_HEIGHT = 494;
const BASE_MEDIA_HEIGHT = 358;
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 screenHeight = ref(812);
const cardScale = ref(1);
const railScrollLeft = ref(0);
const currentScrollLeft = ref(0);
const instance = getCurrentInstance();
let touchStartLeft = 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 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 {
const systemInfo = uni.getSystemInfoSync();
return Number(systemInfo.windowWidth) || 375;
return {
width: Number(systemInfo.windowWidth) || 375,
height: Number(systemInfo.windowHeight) || 812,
};
} 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 targetIndex = clampIndex(index);
if (targetIndex === 0) return 0;
return Math.round(targetIndex * cardStep - getCenterOffset());
return Math.round(targetIndex * cardStep.value - getCenterOffset());
};
const getNearestIndex = (left) =>
clampIndex(Math.round((left + getCenterOffset()) / cardStep));
clampIndex(Math.round((left + getCenterOffset()) / cardStep.value));
const updateScreenWidth = (width) => {
screenWidth.value = Number(width) || getScreenWidth();
const updateLayoutMetrics = async (size = {}) => {
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);
};
const handleWindowResize = (event) => {
updateScreenWidth(event?.size?.windowWidth);
updateLayoutMetrics(event?.size);
};
const setRailScrollLeft = async (targetLeft, force = false) => {
@@ -159,7 +243,7 @@ const handleTouchEnd = () => {
const offset = currentScrollLeft.value - touchStartLeft;
const direction = offset > 0 ? 1 : -1;
const targetIndex =
Math.abs(offset) >= switchThreshold
Math.abs(offset) >= switchThreshold.value
? touchStartIndex + direction
: touchStartIndex;
@@ -180,13 +264,14 @@ watch(
);
onMounted(() => {
updateScreenWidth();
updateLayoutMetrics();
if (typeof uni.onWindowResize === "function") {
uni.onWindowResize(handleWindowResize);
}
});
onUnmounted(() => {
clearTimeout(settleTimer);
if (typeof uni.offWindowResize === "function") {
uni.offWindowResize(handleWindowResize);
}

View File

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

View File

@@ -15,6 +15,7 @@
<script setup>
import { ref } from "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 TemplateCarousel from "./components/TemplateCarousel/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 activeIndex = ref(0);
const giftToastVisible = ref(true);
const giftToastVisible = ref(!getAigcGiftPointsToastClosed());
const templates = aigcTemplates;
const steps = aigcFlowSteps;
@@ -47,6 +48,7 @@ const handleRecharge = () => {
const handleCloseGiftToast = () => {
giftToastVisible.value = false;
setAigcGiftPointsToastClosed();
};
const handleSelectTemplate = (template) => {

View File

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

View File

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