feat(aigc): implement travel AIGC feature and clean up legacy assets

- Replace all local static AIGC assets with OSS hosted URLs to reduce bundle size
- Add travelAIGC command to ChatModel and update quick access menu to link to AIGC home
- Implement AIGC user consent flow with local storage persistence for compliance
- Refactor Discovery page layout, remove unused HeritageDiscoverySection component
- Update ChatTopNavBar to support dynamic navigation bar height based on system info
- Restructure ChatMainList layout for better discovery panel interaction
- Remove deprecated local asset files and clean up unused imports across codebase
This commit is contained in:
duanshuwen
2026-07-06 22:40:36 +08:00
parent bfda50e14a
commit 191ba48b9d
20 changed files with 268 additions and 383 deletions

16
src/constant/aigc.js Normal file
View File

@@ -0,0 +1,16 @@
import { currentClientType } from "@/constant/base";
const CLIENT_TYPE = currentClientType();
const AIGC_CONSENT_AGREED = `${CLIENT_TYPE}_AIGC_CONSENT_AGREED`;
export const getAigcConsentAgreed = () => {
return uni.getStorageSync(AIGC_CONSENT_AGREED);
};
export const setAigcConsentAgreed = (agreed = true) => {
return uni.setStorageSync(AIGC_CONSENT_AGREED, agreed);
};
export const removeAigcConsentAgreed = () => {
return uni.removeStorageSync(AIGC_CONSENT_AGREED);
};

View File

@@ -99,4 +99,6 @@ export const Command = {
feedbackCard: "Command.feedbackCard",
// 生成合成图片
aigcPhotoGenerator: "Command.aigcPhotoGenerator",
// 旅行AIGC
travelAIGC: "Command.travelAIGC",
};

View File

@@ -1,9 +1,7 @@
import glassBoatPostcard from "../../home/assets/glass-boat-postcard.gif";
export const aigcDetailResult = {
id: "glass-boat-postcard-result",
title: "鸳鸯湖明信片",
cover: glassBoatPostcard,
cover: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
accent: "#0a4d46",
variantLabel: "图片版",
resultLabel: "图片版",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 772 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -1,8 +1,3 @@
import glassBoatPostcard from "../assets/glass-boat-postcard.gif";
import bridgeCover from "../assets/card-bridge.png";
import waterfallCover from "../assets/card-waterfall.png";
import cultureCover from "../assets/card-culture-1.png";
export const aigcTemplates = [
{
id: "glass-boat-postcard",
@@ -11,7 +6,7 @@ export const aigcTemplates = [
note: "图片版100积分 · 动效视频版200积分/5秒内",
badge: "同源双版本",
buttonText: "选这个模板",
cover: glassBoatPostcard,
cover: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
tone: "postcard",
accent: "#0a4d46",
},
@@ -22,7 +17,7 @@ export const aigcTemplates = [
note: "图片版100积分 · 动效视频版200积分/5秒内",
badge: "同源双版本",
buttonText: "选这个模板",
cover: bridgeCover,
cover: "https://oss.nianxx.cn/mp/static/xiaoqi/card-bridge.png",
tone: "emerald",
accent: "#0f7069",
},
@@ -33,7 +28,7 @@ export const aigcTemplates = [
note: "图片版100积分 · 动效视频版200积分/5秒内",
badge: "同源双版本",
buttonText: "选这个模板",
cover: waterfallCover,
cover: "https://oss.nianxx.cn/mp/static/xiaoqi/card-waterfall.png",
tone: "aqua",
accent: "#155e75",
},
@@ -44,7 +39,7 @@ export const aigcTemplates = [
note: "图片版100积分 · 动效视频版200积分/5秒内",
badge: "同源双版本",
buttonText: "选这个模板",
cover: cultureCover,
cover: "https://oss.nianxx.cn/mp/static/xiaoqi/card-culture-1.png",
tone: "forest",
accent: "#36563d",
},

View File

@@ -3,10 +3,7 @@
<AigcTopBar :points="pointBalance" @back="handleBack" @history="handleHistory" @recharge="handleRecharge" />
<view class="aigc-home-browser">
<GiftPointsToast
:visible="giftToastVisible"
@close="handleCloseGiftToast"
/>
<GiftPointsToast :visible="giftToastVisible" @close="handleCloseGiftToast" />
<TemplateCarousel v-model="activeIndex" :templates="templates" @select="handleSelectTemplate" />
@@ -29,13 +26,6 @@ const giftToastVisible = ref(true);
const templates = aigcTemplates;
const steps = aigcFlowSteps;
const showPlaceholderToast = (title) => {
uni.showToast({
title,
icon: "none",
});
};
const handleBack = () => {
const pages = getCurrentPages();
if (pages.length > 1) {
@@ -44,11 +34,15 @@ const handleBack = () => {
};
const handleHistory = () => {
showPlaceholderToast("最近任务");
uni.navigateTo({
url: "/pages-aigc/record/record"
});
};
const handleRecharge = () => {
showPlaceholderToast("积分充值");
uni.navigateTo({
url: "/pages-aigc/recharge/recharge"
});
};
const handleCloseGiftToast = () => {
@@ -57,8 +51,7 @@ const handleCloseGiftToast = () => {
const handleSelectTemplate = (template) => {
uni.navigateTo({
url: `/pages-aigc/useTemplate/useTemplate?templateId=${encodeURIComponent(template.id || "")}`,
fail: () => showPlaceholderToast(template.title),
url: `/pages-aigc/useTemplate/useTemplate?templateId=${encodeURIComponent(template.id || "")}`
});
};
</script>

View File

@@ -1,18 +1,16 @@
import glassBoatPostcard from "@/pages-aigc/home/assets/glass-boat-postcard.gif";
export const aigcRecords = [
{
id: "record-lake-postcard",
title: "鸳鸯湖明信片",
time: "2026-06-29 15:46:12",
status: "图片版 · 已完成 · 可升级动效视频",
image: glassBoatPostcard,
image: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
},
{
id: "record-lake-postcard-video",
title: "鸳鸯湖明信片",
time: "2026-06-29 15:58:40",
status: "动效视频版 · 5秒内 · 由图片版升级",
image: glassBoatPostcard,
image: "https://oss.nianxx.cn/mp/static/xiaoqi/glass-boat-postcard.gif",
},
];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 123 KiB

View File

@@ -1,13 +1,21 @@
<template>
<view
v-if="visible"
class="aigc-consent-overlay"
@touchmove.stop.prevent
<uni-popup
ref="popupRef"
type="center"
background-color="transparent"
mask-background-color="rgba(0, 0, 0, 0.58)"
:safe-area="false"
:is-mask-click="false"
@change="handlePopupChange"
>
<view class="aigc-consent-dialog">
<text class="aigc-consent-title">温馨提示</text>
<view
class="aigc-consent-overlay"
@touchmove.stop.prevent
>
<view class="aigc-consent-dialog">
<text class="aigc-consent-title">温馨提示</text>
<view class="aigc-consent-content">
<view class="aigc-consent-content">
<text v-if="content">{{ content }}</text>
<template v-else>
<text>{{ defaultContentPrefix }}</text>
@@ -16,9 +24,9 @@
</text>
<text>{{ defaultContentSuffix }}</text>
</template>
</view>
</view>
<view class="aigc-consent-actions">
<view class="aigc-consent-actions">
<button
class="aigc-consent-button aigc-consent-cancel"
type="default"
@@ -33,13 +41,14 @@
>
我同意
</button>
</view>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { computed } from "vue";
import { computed, nextTick, onMounted, ref, watch } from "vue";
const props = defineProps({
visible: {
@@ -61,6 +70,7 @@ const props = defineProps({
});
const emit = defineEmits(["update:visible", "cancel", "agree", "ruleClick"]);
const popupRef = ref(null);
const defaultContentPrefix = computed(
() =>
@@ -71,6 +81,7 @@ const defaultContentSuffix =
",您点击“我同意”等进行下一步操作的行为均视为您同意受本规则约束。如您不同意,您可以直接关闭页面。";
const close = () => {
popupRef.value?.close();
emit("update:visible", false);
};
@@ -87,6 +98,34 @@ const handleAgree = () => {
const handleRuleClick = () => {
emit("ruleClick");
};
const syncPopupVisible = async (visible) => {
await nextTick();
if (visible) {
popupRef.value?.open();
} else {
popupRef.value?.close();
}
};
const handlePopupChange = (event) => {
const show = event?.show ?? event?.detail?.show;
if (show === false && props.visible) {
emit("update:visible", false);
}
};
watch(
() => props.visible,
(visible) => {
syncPopupVisible(visible);
},
{ immediate: true }
);
onMounted(() => {
syncPopupVisible(props.visible);
});
</script>
<style scoped lang="scss">

View File

@@ -1,18 +1,7 @@
.aigc-consent-overlay {
position: absolute;
z-index: 30;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
padding: 0 28px;
background: rgba(0, 0, 0, 0.58);
box-sizing: border-box;
-webkit-backdrop-filter: blur(1px);
backdrop-filter: blur(1px);
}
.aigc-consent-dialog {

View File

@@ -2,23 +2,17 @@
<view class="flex flex-col h-screen relative overflow-hidden">
<!-- 顶部自定义导航栏 -->
<view class="header absolute top-0 left-0 w-full z-10" :style="{ paddingTop: statusBarHeight + 'px' }">
<ChatTopNavBar ref="topNavBarRef" :mainPageDataModel="mainPageDataModel" @showDrawer="handleShowDrawer" />
<ChatTopNavBar ref="topNavBarRef" :mainPageDataModel="mainPageDataModel" :navHeight="navBarHeight"
@showDrawer="handleShowDrawer" />
</view>
<view class="home-hero relative flex-shrink-0">
<image class="home-hero-bg w-full block" :src="mainPageDataModel?.initPageImages?.backgroundImageUrl"
mode="aspectFill" />
<!-- <SpriteAnimator
v-if="homeSpriteStyle.ipLargeImage"
class="home-hero-sprite"
:src="homeSpriteStyle.ipLargeImage"
:frameWidth="homeSpriteStyle.frameWidth"
:frameHeight="homeSpriteStyle.frameHeight"
:totalFrames="homeSpriteStyle.totalFrames"
:columns="homeSpriteStyle.columns"
:displayWidth="homeSpriteStyle.displayWidth"
:fps="16"
/> -->
<SpriteAnimator v-if="homeSpriteStyle.ipLargeImage" class="home-hero-sprite" :src="homeSpriteStyle.ipLargeImage"
:frameWidth="homeSpriteStyle.frameWidth" :frameHeight="homeSpriteStyle.frameHeight"
:totalFrames="homeSpriteStyle.totalFrames" :columns="homeSpriteStyle.columns"
:displayWidth="homeSpriteStyle.displayWidth" :fps="16" />
<view class="home-tabs-layer absolute bottom-0 left-0 right-0 flex-full">
<view style="margin-bottom: -1px;">
<AiTabSwitch v-model="tabIndex" :list="tabList" @change="handleChange" />
@@ -26,16 +20,16 @@
</view>
</view>
<scroll-view v-show="tabIndex === 0" class="main-scroll-area flex flex-col flex-full overflow-hidden" scroll-y
@scroll="handleDiscoveryScroll"
@touchstart.capture="handleScrollAreaTouchStart" @touchstart="handleScrollAreaTouchStart"
@touchmove="handleScrollAreaTouchMove">
<view class="home-welcome-collapse" :class="{ 'is-collapsed': isHomeWelcomeCollapsed }">
<view v-show="tabIndex === 0" class="discovery-shell flex-full" @touchstart.capture="handleDiscoveryPanelTouchStart"
@touchstart="handleDiscoveryPanelTouchStart" @touchmove="handleDiscoveryPanelTouchMove">
<view class="home-welcome-panel">
<HomeWelcome class="flex-shrink-0" :mainPageDataModel="mainPageDataModel" />
</view>
<Discovery />
</scroll-view>
<view class="discovery-panel" :class="{ 'is-collapsed': isDiscoveryPanelCollapsed }">
<Discovery @content-scroll="handleDiscoveryContentScroll" />
</view>
</view>
<!-- 消息列表可滚动区域 -->
<scroll-view v-show="tabIndex === 1" class="main main-scroll-area flex-full overflow-hidden min-height-0" scroll-y
@@ -215,6 +209,7 @@ const emit = defineEmits(["showDrawer"]);
const appStore = useAppStore();
/// 导航栏相关
const statusBarHeight = ref(20);
const navBarHeight = ref(44);
/// 输入框组件引用
const inputAreaRef = ref(null);
const topNavBarRef = ref();
@@ -230,12 +225,11 @@ const isKeyboardShow = ref(false);
///(控制滚动位置)
const scrollTop = ref(99999);
const discoveryScrollTop = ref(0);
const lastDiscoveryScrollTop = ref(0);
const isHomeWelcomeCollapsed = ref(false);
const DISCOVERY_WELCOME_COLLAPSE_TOP = 12;
const DISCOVERY_WELCOME_EXPAND_TOP = 4;
const DISCOVERY_SCROLL_DELTA = 2;
const discoveryInnerScrollTop = ref(0);
const discoveryPanelTouchStartY = ref(0);
const isDiscoveryPanelCollapsed = ref(false);
const DISCOVERY_PANEL_EXPAND_TOP = 4;
const DISCOVERY_PANEL_TOUCH_DELTA = 16;
/// 会话列表
const chatMsgList = ref([]);
@@ -302,6 +296,9 @@ const homeSpriteStyle = computed(() => {
const handleChange = (i) => {
console.log("切换:", i);
if (i === 0) {
resetDiscoveryScrollState();
}
};
const handleShowDrawer = () => {
@@ -389,26 +386,45 @@ const handleScrollAreaTouchMove = () => {
hideKeyboardByScroll();
};
const resetDiscoveryScrollState = () => {
discoveryInnerScrollTop.value = 0;
discoveryPanelTouchStartY.value = 0;
isDiscoveryPanelCollapsed.value = false;
};
// 处理用户滚动事件
const handleDiscoveryScroll = (e) => {
if (Date.now() - lastScrollTouchAt < 1000) {
hideKeyboardByScroll();
}
const getTouchPointY = (e) => {
const touch = e?.touches?.[0] || e?.changedTouches?.[0];
return Number(touch?.clientY ?? touch?.pageY ?? 0);
};
const currentScrollTop = Number(e.detail?.scrollTop || 0);
const scrollDelta = currentScrollTop - lastDiscoveryScrollTop.value;
discoveryScrollTop.value = currentScrollTop;
const handleDiscoveryPanelTouchStart = (e) => {
handleScrollAreaTouchStart();
discoveryPanelTouchStartY.value = getTouchPointY(e);
};
if (currentScrollTop <= DISCOVERY_WELCOME_EXPAND_TOP) {
isHomeWelcomeCollapsed.value = false;
const handleDiscoveryPanelTouchMove = (e) => {
handleScrollAreaTouchMove();
const currentY = getTouchPointY(e);
if (!currentY || !discoveryPanelTouchStartY.value) return;
const touchDelta = currentY - discoveryPanelTouchStartY.value;
if (touchDelta < -DISCOVERY_PANEL_TOUCH_DELTA) {
isDiscoveryPanelCollapsed.value = true;
} else if (
currentScrollTop > DISCOVERY_WELCOME_COLLAPSE_TOP ||
scrollDelta > DISCOVERY_SCROLL_DELTA
touchDelta > DISCOVERY_PANEL_TOUCH_DELTA &&
discoveryInnerScrollTop.value <= DISCOVERY_PANEL_EXPAND_TOP
) {
isHomeWelcomeCollapsed.value = true;
isDiscoveryPanelCollapsed.value = false;
}
};
lastDiscoveryScrollTop.value = currentScrollTop;
const handleDiscoveryContentScroll = ({ scrollTop }) => {
discoveryInnerScrollTop.value = Number(scrollTop || 0);
if (discoveryInnerScrollTop.value > DISCOVERY_PANEL_TOUCH_DELTA) {
isDiscoveryPanelCollapsed.value = true;
}
};
const welcomeHeight = ref(0);
@@ -537,11 +553,19 @@ const addNoticeListener = () => {
/// =============生命周期函数↓================
onLoad(() => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20;
},
});
const systemInfo = uni.getSystemInfoSync();
const systemStatusBarHeight = systemInfo.statusBarHeight || 20;
statusBarHeight.value = systemStatusBarHeight;
navBarHeight.value = systemInfo.platform === "ios" ? 44 : 48;
if (typeof uni.getMenuButtonBoundingClientRect === "function") {
const menuButton = uni.getMenuButtonBoundingClientRect();
if (menuButton?.height && typeof menuButton.top === "number") {
statusBarHeight.value = Math.max(menuButton.top, systemStatusBarHeight);
navBarHeight.value = menuButton.height;
}
}
});
onReady(() => {

View File

@@ -5,23 +5,41 @@
overscroll-behavior-y: contain;
}
.home-welcome-collapse {
width: 100%;
height: 92px;
flex-shrink: 0;
.discovery-shell {
position: relative;
flex-basis: 0;
height: 0;
min-height: 0;
overflow: hidden;
opacity: 1;
transform: translateY(0);
transition:
height 240ms ease,
opacity 240ms ease,
transform 240ms ease;
background: $theme-color-50;
}
.home-welcome-collapse.is-collapsed {
height: 0;
opacity: 0;
transform: translateY(-8px);
.home-welcome-panel {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 1;
width: 100%;
height: 92px;
overflow: hidden;
}
.discovery-panel {
position: absolute;
top: 92px;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
min-height: 0;
overflow: hidden;
background: $theme-color-50;
transition: top 240ms ease;
}
.discovery-panel.is-collapsed {
top: 0;
}
.home-hero {

View File

@@ -1,14 +1,21 @@
<template>
<view class="quick-access flex flex-row ml-12 pt-8 pb-8 scroll-x whitespace-nowrap">
<view class="item border-box rounded-50 flex flex-row items-center" v-for="(item, index) in itemList" :key="index"
@click="sendReply(item)">
<view class="flex items-center justify-center">
<image v-if="item.icon" class="icon" :src="item.icon" />
<text class="font-size-14 theme-color-500 line-height-20">
{{ item.title }}
</text>
<view>
<view class="quick-access flex flex-row ml-12 pt-8 pb-8 scroll-x whitespace-nowrap">
<view class="item border-box rounded-50 flex flex-row items-center" v-for="(item, index) in itemList" :key="index"
@click="sendReply(item)">
<view class="flex items-center justify-center">
<image v-if="item.icon" class="icon" :src="item.icon" />
<text class="font-size-14 theme-color-500 line-height-20">
{{ item.title }}
</text>
</view>
</view>
</view>
<AigcConsentDialog
v-model:visible="aigcConsentVisible"
@agree="handleAigcConsentAgree"
/>
</view>
</template>
@@ -16,7 +23,12 @@
import { ref } from "vue";
import { Command } from "@/model/ChatModel";
import { SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
import { getAigcConsentAgreed, setAigcConsentAgreed } from "@/constant/aigc";
import { checkToken } from "@/hooks/useGoLogin";
import AigcConsentDialog from "../AigcConsentDialog/index.vue";
const AIGC_HOME_URL = "/pages-aigc/home/home";
const aigcConsentVisible = ref(false);
const itemList = ref([
{
@@ -26,8 +38,8 @@ const itemList = ref([
},
{
icon: "",
title: "我的订单",
type: Command.myOrder,
title: "旅行记录",
type: Command.travelAIGC,
},
{
icon: "",
@@ -64,8 +76,29 @@ const sendReply = (item) => {
return;
}
// 旅行AIGC
if (item.type === Command.travelAIGC) {
checkToken().then(() => {
if (getAigcConsentAgreed()) {
navigateToAigcHome();
} else {
aigcConsentVisible.value = true;
}
});
return;
}
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, item);
};
const navigateToAigcHome = () => {
uni.navigateTo({ url: AIGC_HOME_URL });
};
const handleAigcConsentAgree = () => {
setAigcConsentAgreed(true);
navigateToAigcHome();
};
</script>
<style lang="scss" scoped>

View File

@@ -1,5 +1,5 @@
<template>
<view class="border-box h-44 flex flex-items-center pl-12 pr-12">
<view class="border-box h-44 flex flex-items-center pl-12 pr-12" :style="navBarStyle">
<view class="nav-icon-button" @tap.stop="showDrawer">
<uni-icons type="bars" size="24" color="#ffffff" />
</view>
@@ -29,6 +29,10 @@ import { getCurrentConfig } from "@/constant/base";
import SpriteAnimator from "@/components/Sprite/SpriteAnimator.vue";
const props = defineProps({
navHeight: {
type: Number,
default: 44,
},
mainPageDataModel: {
type: Object,
default: () => ({
@@ -60,6 +64,10 @@ const emit = defineEmits(["showDrawer"]);
const showDrawer = () => emit("showDrawer");
const navBarStyle = computed(() => ({
height: `${props.navHeight}px`,
}));
defineExpose({ show });
</script>

View File

@@ -1,111 +0,0 @@
<template>
<view class="heritage-section">
<view class="heritage-carousel">
<scroll-view
class="heritage-scroll"
scroll-x
show-scrollbar="false"
:scroll-left="carouselScrollLeft"
>
<view class="heritage-track">
<view
v-for="(item, index) in normalizedCards"
:key="getItemKey(item, index)"
class="heritage-card"
:class="{
'is-featured': index === 1,
'is-selected': index === 1,
}"
hover-class="is-pressed"
hover-stay-time="80"
@tap="handleCardTap(item.raw, index)"
>
<view class="heritage-image">
<image
v-if="item.coverImage"
class="heritage-cover"
:src="item.coverImage"
mode="aspectFill"
/>
<text class="heritage-badge">{{ item.badge }}</text>
</view>
<view class="heritage-copy">
<text class="heritage-tag ellipsis-1">{{ item.tag }}</text>
<text class="heritage-title ellipsis-1">{{ item.title }}</text>
<text class="heritage-desc ellipsis-1">{{ item.subTitle }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
<HeritageQuickPrompts
:list="prompts"
@didSelectItem="handlePromptTap"
/>
</view>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import HeritageQuickPrompts from "../HeritageQuickPrompts/index.vue";
const props = defineProps({
cards: {
type: Array,
default: () => [],
},
prompts: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["select-card", "select-prompt"]);
const carouselScrollLeft = ref(165);
watch(
() => props.cards.length,
(length) => {
const targetScrollLeft = length > 1 ? 165 : 0;
carouselScrollLeft.value = 0;
setTimeout(() => {
carouselScrollLeft.value = targetScrollLeft;
}, 0);
},
{ immediate: true }
);
const getBadgeText = (item) => {
const source = item.jumpDesc || item.tag || item.title || "";
return source.slice(0, 2) || "推荐";
};
const normalizedCards = computed(() =>
props.cards.map((item) => ({
raw: item,
id: item.id ?? item.tabContentId,
title: item.title || "",
subTitle: item.subTitle || "",
tag: item.tag || item.jumpDesc || "推荐",
badge: getBadgeText(item),
coverImage: item.coverImage || "",
}))
);
const getItemKey = (item, index) => item.id ?? item.title ?? index;
const handleCardTap = (item, index) => {
emit("select-card", item, index);
};
const handlePromptTap = (item, index) => {
emit("select-prompt", item, index);
};
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -1,137 +0,0 @@
.heritage-section {
width: 100%;
height: 386px;
overflow: hidden;
background: $theme-color-50;
}
.heritage-carousel,
.heritage-scroll {
width: 100%;
height: 268px;
overflow: hidden;
background: $theme-color-50;
}
.heritage-track {
display: inline-flex;
align-items: flex-start;
min-width: 100%;
height: 268px;
padding: 8px 69px 26px;
box-sizing: border-box;
}
.heritage-card {
flex: 0 0 189px;
width: 189px;
height: 227px;
margin-top: 5px;
margin-right: 14px;
display: flex;
flex-direction: column;
padding: 8px;
border-radius: 28px;
border: 1px solid #f1f5f9;
background: #ffffff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
box-sizing: border-box;
text-align: left;
}
.heritage-card:last-child {
margin-right: 0;
}
.heritage-card.is-featured {
flex-basis: 237px;
width: 237px;
height: 234px;
margin-top: 0;
}
.heritage-card.is-selected {
border-color: rgba(12, 205, 88, 0.55);
box-shadow: 0 8px 18px rgba(12, 205, 88, 0.12);
transform: translateY(-2px);
}
.heritage-card.is-pressed {
opacity: 0.92;
}
.heritage-image {
position: relative;
width: 100%;
height: 138px;
overflow: hidden;
border-radius: 20px;
background: #f1f5f9;
}
.heritage-card.is-featured .heritage-image {
height: 145px;
}
.heritage-cover {
width: 100%;
height: 100%;
display: block;
}
.heritage-badge {
position: absolute;
top: 8px;
right: 8px;
width: 34px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
color: $theme-color-500;
font-size: 11px;
line-height: 16px;
font-weight: 900;
}
.heritage-copy {
min-width: 0;
margin-top: 8px;
padding: 0 8px;
}
.heritage-tag {
display: inline-flex;
max-width: 100%;
height: 18px;
align-items: center;
padding: 0 6px;
border-radius: 4px;
background: #fffbeb;
color: #d97706;
font-size: 9px;
line-height: 14px;
font-weight: 800;
}
.heritage-title {
display: block;
max-width: 100%;
margin: 4px 0;
color: #1e293b;
font-size: 16px;
line-height: 20px;
font-weight: 900;
}
.heritage-desc {
display: block;
max-width: 100%;
color: #94a3b8;
font-size: 10px;
line-height: 15px;
font-weight: 600;
}

View File

@@ -4,17 +4,18 @@
<FindTabs v-if="discoveryTabs.length > 0" v-model="activeIndex" :tabs="discoveryTabs" @change="handleTabChange" />
</view>
<HeritageDiscoverySection :cards="discoveryCards" :prompts="discoveryQuickQuestions" @select-card="handleCardClick"
@select-prompt="handleQuickQuestionClick" />
<scroll-view class="discovery-scroll" scroll-y @scroll="handleContentScroll">
<view class="discovery-body">
<ExploreCards v-if="discoveryCards.length > 0" :list="discoveryCards" @didSelectItem="handleCardClick" />
<ExploreCards v-if="discoveryCards.length > 0" :list="discoveryCards" @didSelectItem="handleCardClick" />
<QuickQuestions v-if="discoveryQuickQuestions.length > 0" :list="discoveryQuickQuestions"
@didSelectItem="handleQuickQuestionClick" />
<QuickQuestions v-if="discoveryQuickQuestions.length > 0" :list="discoveryQuickQuestions"
@didSelectItem="handleQuickQuestionClick" />
<DiscoveryPhotoToolCard @didSelectItem="handleToolCardClick" @didSelectOption="handleToolOptionClick" />
<!-- <DiscoveryPhotoToolCard @didSelectItem="handleToolCardClick" @didSelectOption="handleToolOptionClick" />
<DiscoveryAudioToolCard @didSelectItem="handleToolCardClick" />
<DiscoveryGuideMapToolCard @didSelectItem="handleToolCardClick" />
<DiscoveryGuideMapToolCard @didSelectItem="handleToolCardClick" /> -->
</view>
</scroll-view>
</view>
</template>
@@ -26,7 +27,6 @@ import { getAccessToken } from "@/constant/token";
import FindTabs from "./components/FindTabs/index.vue";
import ExploreCards from "./components/ExploreCards/index.vue";
import HeritageDiscoverySection from "./components/HeritageDiscoverySection/index.vue";
import QuickQuestions from "./components/QuickQuestions/index.vue";
import DiscoveryPhotoToolCard from "./components/DiscoveryPhotoToolCard/index.vue";
import DiscoveryAudioToolCard from "./components/DiscoveryAudioToolCard/index.vue";
@@ -45,7 +45,13 @@ const activeIndex = ref(0);
const discoveryTabs = ref([]);
const discoveryCards = ref([]);
const discoveryQuickQuestions = ref([]);
const emit = defineEmits(["scroll-touch-start", "scroll-touch"]);
const emit = defineEmits(["content-scroll"]);
const handleContentScroll = (e) => {
emit("content-scroll", {
scrollTop: Number(e.detail?.scrollTop || 0),
});
};
/// tabs 切换事件
@@ -208,16 +214,30 @@ onMounted(() => {
<style lang="scss" scoped>
.discovery-content {
display: flex;
flex-direction: column;
width: 100%;
min-height: 100%;
height: 100%;
min-height: 0;
background: $theme-color-50;
}
.discovery-tabs-sticky {
position: sticky;
top: 0;
flex-shrink: 0;
z-index: 12;
width: 100%;
background: $theme-color-50;
}
.discovery-scroll {
flex: 1;
width: 100%;
height: 0;
min-height: 0;
overflow: hidden;
}
.discovery-body {
width: 100%;
}
</style>