Files
YGChatCS/src/pages-aigc/home/components/TemplateCarousel/index.vue
duanshuwen f47f68e19b feat(aigc): add travel postcard AIGC feature
Add complete travel postcard AIGC creation workflow including:
- New AIGC pages: template selection home, result detail, and history records
- Reusable UI components: template card, flow steps, result preview, action buttons, record lists
- Static assets including template images, fonts and styles
- Updated TopNavBar component to support menu button alignment
- Register new AIGC page routes in pages.json
- Add mock data for templates, user records and result details
2026-07-05 17:47:37 +08:00

199 lines
4.8 KiB
Vue

<template>
<view class="template-carousel">
<view class="template-heading">
<view>
<text class="template-heading-title">选择模板</text>
<text class="template-heading-subtitle">左右滑动预览效果</text>
</view>
<text class="template-heading-badge">同源双版本</text>
</view>
<scroll-view
class="template-rail"
scroll-x
show-scrollbar="false"
scroll-with-animation
:scroll-left="railScrollLeft"
@scroll="handleScroll"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
@touchcancel="handleTouchEnd"
>
<view class="template-track">
<TemplateCard
v-for="(template, index) in templates"
:key="template.id || index"
:template="template"
:active="index === modelValue"
@select="handleSelect(template, index)"
/>
</view>
</scroll-view>
<view class="template-dots">
<view
v-for="(template, index) in templates"
:key="`${template.id || index}-dot`"
class="template-dot"
:class="{ 'is-active': index === modelValue }"
/>
</view>
</view>
</template>
<script setup>
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import TemplateCard from "../TemplateCard/index.vue";
const props = defineProps({
templates: {
type: Array,
default: () => [],
},
modelValue: {
type: Number,
default: 0,
},
});
const emit = defineEmits(["update:modelValue", "select"]);
const cardStep = 330;
const cardWidth = 318;
const sidePadding = 16;
const switchThreshold = 88;
const screenWidth = ref(375);
const railScrollLeft = ref(0);
const currentScrollLeft = ref(0);
let touchStartLeft = 0;
let touchStartIndex = 0;
let isTouching = false;
let settleTimer = null;
const getMaxIndex = () => Math.max(props.templates.length - 1, 0);
const clampIndex = (index) => Math.max(0, Math.min(getMaxIndex(), index));
const getScreenWidth = () => {
try {
const systemInfo = uni.getSystemInfoSync();
return Number(systemInfo.windowWidth) || 375;
} catch (error) {
return 375;
}
};
const getCenterOffset = () => (screenWidth.value - cardWidth) / 2 - sidePadding;
const getTargetScrollLeft = (index) => {
const targetIndex = clampIndex(index);
if (targetIndex === 0) return 0;
return Math.round(targetIndex * cardStep - getCenterOffset());
};
const getNearestIndex = (left) =>
clampIndex(Math.round((left + getCenterOffset()) / cardStep));
const updateScreenWidth = (width) => {
screenWidth.value = Number(width) || getScreenWidth();
scrollToIndex(props.modelValue);
};
const handleWindowResize = (event) => {
updateScreenWidth(event?.size?.windowWidth);
};
const setRailScrollLeft = async (targetLeft, force = false) => {
await nextTick();
if (force && railScrollLeft.value === targetLeft) {
railScrollLeft.value = targetLeft > 0 ? targetLeft - 1 : 1;
setTimeout(() => {
railScrollLeft.value = targetLeft;
currentScrollLeft.value = targetLeft;
}, 0);
return;
}
railScrollLeft.value = targetLeft;
currentScrollLeft.value = targetLeft;
};
const scrollToIndex = (index, force = false) => {
setRailScrollLeft(getTargetScrollLeft(index), force);
};
const settleToIndex = (index, force = true) => {
const targetIndex = clampIndex(index);
if (targetIndex !== props.modelValue) {
emit("update:modelValue", targetIndex);
}
scrollToIndex(targetIndex, force);
};
const handleScroll = (event) => {
currentScrollLeft.value = event.detail?.scrollLeft || 0;
if (!isTouching) {
clearTimeout(settleTimer);
settleTimer = setTimeout(() => {
settleToIndex(getNearestIndex(currentScrollLeft.value));
}, 140);
}
};
const handleTouchStart = () => {
clearTimeout(settleTimer);
isTouching = true;
touchStartLeft = currentScrollLeft.value;
touchStartIndex = clampIndex(props.modelValue);
};
const handleTouchEnd = () => {
if (!isTouching) return;
isTouching = false;
clearTimeout(settleTimer);
const offset = currentScrollLeft.value - touchStartLeft;
const direction = offset > 0 ? 1 : -1;
const targetIndex =
Math.abs(offset) >= switchThreshold
? touchStartIndex + direction
: touchStartIndex;
settleToIndex(targetIndex);
};
const handleSelect = (template, index) => {
settleToIndex(index);
emit("select", template, index);
};
watch(
() => [props.modelValue, props.templates.length],
() => {
scrollToIndex(props.modelValue);
},
{ immediate: true }
);
onMounted(() => {
updateScreenWidth();
if (typeof uni.onWindowResize === "function") {
uni.onWindowResize(handleWindowResize);
}
});
onUnmounted(() => {
if (typeof uni.offWindowResize === "function") {
uni.offWindowResize(handleWindowResize);
}
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>