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

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>