Merge branch 'home3.0' of https://git.nianxx.cn/zoujing/YGChatCS into aigc

This commit is contained in:
duanshuwen
2026-07-14 22:09:53 +08:00
13 changed files with 249 additions and 30 deletions

View File

@@ -0,0 +1,114 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
const helperUrl = new URL("../src/utils/discoveryPanelScroll.js", import.meta.url);
const chatMainListUrl = new URL("../src/pages/ChatMain/ChatMainList/index.vue", import.meta.url);
const helperSource = await readFile(helperUrl, "utf8");
const chatMainListSource = await readFile(chatMainListUrl, "utf8");
const helperModule = await import(
`data:text/javascript;charset=utf-8,${encodeURIComponent(helperSource)}`
);
const {
canExpandDiscoveryPanelFromScroll,
getDiscoveryPanelCollapseState,
} = helperModule;
assert.equal(
getDiscoveryPanelCollapseState({
currentScrollTop: 24,
lastScrollTop: 0,
isCollapsed: false,
threshold: 16,
}),
true,
"scrolling up into discovery content should collapse the welcome panel"
);
assert.equal(
getDiscoveryPanelCollapseState({
currentScrollTop: 18,
lastScrollTop: 15,
isCollapsed: false,
threshold: 16,
}),
true,
"slow upward scrolling past the threshold should collapse the welcome panel"
);
assert.equal(
getDiscoveryPanelCollapseState({
currentScrollTop: 40,
lastScrollTop: 64,
isCollapsed: true,
threshold: 16,
allowExpand: true,
}),
false,
"scrolling down should reveal the welcome panel"
);
assert.equal(
getDiscoveryPanelCollapseState({
currentScrollTop: 40,
lastScrollTop: 64,
isCollapsed: true,
threshold: 16,
allowExpand: false,
}),
true,
"layout-driven reverse scroll after release should not reveal the welcome panel"
);
assert.equal(
getDiscoveryPanelCollapseState({
currentScrollTop: 63,
lastScrollTop: 64,
isCollapsed: true,
threshold: 16,
}),
true,
"small scroll jitter should keep the current panel state"
);
assert.match(
chatMainListSource,
/@touchmove\.capture="handleDiscoveryPanelTouchMove"/,
"discovery shell should capture touchmove before nested scroll-view consumes the gesture"
);
assert.equal(
canExpandDiscoveryPanelFromScroll({
isTouching: false,
touchDirection: "",
lastDownGestureAt: 1000,
now: 1400,
gestureRetainMs: 800,
}),
true,
"momentum scroll after a down gesture should still be allowed to reveal the welcome panel"
);
assert.equal(
canExpandDiscoveryPanelFromScroll({
isTouching: false,
touchDirection: "",
lastDownGestureAt: 1000,
now: 1901,
gestureRetainMs: 800,
}),
false,
"old reverse scroll should not be treated as an active down gesture"
);
assert.equal(
canExpandDiscoveryPanelFromScroll({
isTouching: true,
touchDirection: "up",
lastDownGestureAt: 1000,
now: 1200,
gestureRetainMs: 800,
}),
false,
"an active upward gesture should block reveal even if a down gesture happened earlier"
);

View File

@@ -21,7 +21,9 @@
</view> </view>
<view v-show="tabIndex === 0" class="discovery-shell flex-full" @touchstart.capture="handleDiscoveryPanelTouchStart" <view v-show="tabIndex === 0" class="discovery-shell flex-full" @touchstart.capture="handleDiscoveryPanelTouchStart"
@touchstart="handleDiscoveryPanelTouchStart" @touchmove="handleDiscoveryPanelTouchMove"> @touchstart="handleDiscoveryPanelTouchStart" @touchmove.capture="handleDiscoveryPanelTouchMove"
@touchmove="handleDiscoveryPanelTouchMove"
@touchend="handleDiscoveryPanelTouchEnd" @touchcancel="handleDiscoveryPanelTouchEnd">
<view class="home-welcome-panel"> <view class="home-welcome-panel">
<HomeWelcome class="flex-shrink-0" :mainPageDataModel="mainPageDataModel" /> <HomeWelcome class="flex-shrink-0" :mainPageDataModel="mainPageDataModel" />
</view> </view>
@@ -196,6 +198,10 @@ import {
recentConversation, recentConversation,
} from "@/request/api/ConversationApi"; } from "@/request/api/ConversationApi";
import WebSocketManager from "@/utils/WebSocketManager"; import WebSocketManager from "@/utils/WebSocketManager";
import {
canExpandDiscoveryPanelFromScroll,
getDiscoveryPanelCollapseState,
} from "@/utils/discoveryPanelScroll";
import { IdUtils } from "@/utils"; import { IdUtils } from "@/utils";
import { import {
appendLongTextChunk, appendLongTextChunk,
@@ -228,8 +234,14 @@ const scrollTop = ref(99999);
const discoveryInnerScrollTop = ref(0); const discoveryInnerScrollTop = ref(0);
const discoveryPanelTouchStartY = ref(0); const discoveryPanelTouchStartY = ref(0);
const isDiscoveryPanelCollapsed = ref(false); const isDiscoveryPanelCollapsed = ref(false);
let lastDiscoveryInnerScrollTop = 0;
let isDiscoveryPanelTouching = false;
let discoveryPanelTouchDirection = "";
let lastDiscoveryDownGestureAt = 0;
const DISCOVERY_PANEL_EXPAND_TOP = 4; const DISCOVERY_PANEL_EXPAND_TOP = 4;
const DISCOVERY_PANEL_TOUCH_DELTA = 16; const DISCOVERY_PANEL_TOUCH_DELTA = 16;
const DISCOVERY_PANEL_DIRECTION_DELTA = 2;
const DISCOVERY_PANEL_GESTURE_RETAIN_MS = 800;
/// 会话列表 /// 会话列表
const chatMsgList = ref([]); const chatMsgList = ref([]);
@@ -389,6 +401,10 @@ const handleScrollAreaTouchMove = () => {
const resetDiscoveryScrollState = () => { const resetDiscoveryScrollState = () => {
discoveryInnerScrollTop.value = 0; discoveryInnerScrollTop.value = 0;
discoveryPanelTouchStartY.value = 0; discoveryPanelTouchStartY.value = 0;
lastDiscoveryInnerScrollTop = 0;
isDiscoveryPanelTouching = false;
discoveryPanelTouchDirection = "";
lastDiscoveryDownGestureAt = 0;
isDiscoveryPanelCollapsed.value = false; isDiscoveryPanelCollapsed.value = false;
}; };
@@ -400,6 +416,8 @@ const getTouchPointY = (e) => {
const handleDiscoveryPanelTouchStart = (e) => { const handleDiscoveryPanelTouchStart = (e) => {
handleScrollAreaTouchStart(); handleScrollAreaTouchStart();
isDiscoveryPanelTouching = true;
discoveryPanelTouchDirection = "";
discoveryPanelTouchStartY.value = getTouchPointY(e); discoveryPanelTouchStartY.value = getTouchPointY(e);
}; };
@@ -410,6 +428,14 @@ const handleDiscoveryPanelTouchMove = (e) => {
if (!currentY || !discoveryPanelTouchStartY.value) return; if (!currentY || !discoveryPanelTouchStartY.value) return;
const touchDelta = currentY - discoveryPanelTouchStartY.value; const touchDelta = currentY - discoveryPanelTouchStartY.value;
if (touchDelta < -DISCOVERY_PANEL_DIRECTION_DELTA) {
discoveryPanelTouchDirection = "up";
lastDiscoveryDownGestureAt = 0;
} else if (touchDelta > DISCOVERY_PANEL_DIRECTION_DELTA) {
discoveryPanelTouchDirection = "down";
lastDiscoveryDownGestureAt = Date.now();
}
if (touchDelta < -DISCOVERY_PANEL_TOUCH_DELTA) { if (touchDelta < -DISCOVERY_PANEL_TOUCH_DELTA) {
isDiscoveryPanelCollapsed.value = true; isDiscoveryPanelCollapsed.value = true;
} else if ( } else if (
@@ -420,11 +446,27 @@ const handleDiscoveryPanelTouchMove = (e) => {
} }
}; };
const handleDiscoveryPanelTouchEnd = () => {
isDiscoveryPanelTouching = false;
discoveryPanelTouchDirection = "";
};
const handleDiscoveryContentScroll = ({ scrollTop }) => { const handleDiscoveryContentScroll = ({ scrollTop }) => {
discoveryInnerScrollTop.value = Number(scrollTop || 0); const currentScrollTop = Number(scrollTop || 0);
if (discoveryInnerScrollTop.value > DISCOVERY_PANEL_TOUCH_DELTA) { isDiscoveryPanelCollapsed.value = getDiscoveryPanelCollapseState({
isDiscoveryPanelCollapsed.value = true; currentScrollTop,
} lastScrollTop: lastDiscoveryInnerScrollTop,
isCollapsed: isDiscoveryPanelCollapsed.value,
threshold: DISCOVERY_PANEL_TOUCH_DELTA,
allowExpand: canExpandDiscoveryPanelFromScroll({
isTouching: isDiscoveryPanelTouching,
touchDirection: discoveryPanelTouchDirection,
lastDownGestureAt: lastDiscoveryDownGestureAt,
gestureRetainMs: DISCOVERY_PANEL_GESTURE_RETAIN_MS,
}),
});
discoveryInnerScrollTop.value = currentScrollTop;
lastDiscoveryInnerScrollTop = currentScrollTop;
}; };
const welcomeHeight = ref(0); const welcomeHeight = ref(0);

View File

@@ -43,12 +43,12 @@
} }
.home-hero { .home-hero {
height: 136px; height: 140px;
overflow: hidden; overflow: hidden;
} }
.home-hero-bg { .home-hero-bg {
height: 136px; height: 140px;
} }
.home-hero-sprite { .home-hero-sprite {

View File

@@ -36,10 +36,15 @@ const itemList = ref([
title: "快速预定", title: "快速预定",
type: Command.quickBooking, type: Command.quickBooking,
}, },
// {
// icon: "",
// title: "旅行记录",
// type: Command.travelAIGC,
// },
{ {
icon: "", icon: "",
title: "旅行记录", title: "我的订单",
type: Command.travelAIGC, type: Command.myOrder,
}, },
{ {
icon: "", icon: "",

View File

@@ -1,7 +1,7 @@
<template> <template>
<view v-if="hasBannerList"> <view v-if="hasBannerList">
<swiper @change="onSwiperChange" class="swiper" circular :autoplay="autoplay" :interval="interval" <swiper @change="onSwiperChange" class="swiper" circular :autoplay="autoplay" :interval="interval"
:duration="duration" :indicator-dots="false"> :duration="duration" :indicator-dots="false" :vertical="true">
<swiper-item v-for="item in bannerList" :key="item.entityName"> <swiper-item v-for="item in bannerList" :key="item.entityName">
<view class="swiper-item" @click="clickItem(item)"> <view class="swiper-item" @click="clickItem(item)">
<text class="text-color font-size-12 font-600"> <text class="text-color font-size-12 font-600">

View File

@@ -5,7 +5,7 @@
.swiper-stage { .swiper-stage {
position: relative; position: relative;
width: 100%; width: 100%;
height: 270px; height: 265px;
overflow: hidden; overflow: hidden;
} }
@@ -33,7 +33,7 @@
overflow: hidden; overflow: hidden;
border-radius: 24px; border-radius: 24px;
background: #ffffff; background: #ffffff;
box-shadow: 0 12px 20px rgba(15, 23, 42, 0.14); box-shadow: 0 12px 16px rgba(15, 23, 42, 0.14);
} }
.card-media { .card-media {
@@ -93,7 +93,7 @@
} }
.is-current .card-shell { .is-current .card-shell {
box-shadow: 0 12px 20px rgba(15, 23, 42, 0.18); box-shadow: 0 12px 16px rgba(15, 23, 42, 0.18);
} }
.is-single .swiper-stage { .is-single .swiper-stage {

View File

@@ -1,9 +1,9 @@
.discovery-audio-tool-card { .discovery-audio-tool-card {
width: 100%; width: 100%;
height: 96px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
box-sizing: border-box; box-sizing: border-box;
padding: 0 12px;
} }
.discovery-audio-tool-card.is-pressed { .discovery-audio-tool-card.is-pressed {
@@ -28,7 +28,7 @@
height: 88px; height: 88px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 18px 12px 14px; padding: 12px;
border-radius: 28px; border-radius: 28px;
background: #ffffff; background: #ffffff;
box-sizing: border-box; box-sizing: border-box;
@@ -94,7 +94,6 @@
.audio-progress-row { .audio-progress-row {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: 16px;
} }
.audio-progress-track { .audio-progress-track {
@@ -114,9 +113,9 @@
.audio-time { .audio-time {
flex: 0 0 90px; flex: 0 0 90px;
max-width: 90px; max-width: 90px;
margin-left: 12px; margin-left: 10px;
color: #94a0b2; color: #94a0b2;
font-size: 15px; font-size: 12px;
line-height: 20px; line-height: 20px;
font-weight: 600; font-weight: 600;
text-align: right; text-align: right;

View File

@@ -4,6 +4,7 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
box-sizing: border-box; box-sizing: border-box;
padding: 0 12px;
} }
.discovery-guide-map-tool-card.is-pressed { .discovery-guide-map-tool-card.is-pressed {

View File

@@ -4,6 +4,7 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
box-sizing: border-box; box-sizing: border-box;
padding: 0 12px;
} }
.discovery-photo-tool-card.is-pressed { .discovery-photo-tool-card.is-pressed {
@@ -28,7 +29,7 @@
height: 142px; height: 142px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 16px 12px 12px; padding: 12px;
border-radius: 28px; border-radius: 28px;
background: #ffffff; background: #ffffff;
box-sizing: border-box; box-sizing: border-box;
@@ -197,7 +198,7 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
margin-top: 10px; margin-top: 6px;
} }
.photo-option { .photo-option {
@@ -250,7 +251,7 @@
.photo-option-label { .photo-option-label {
display: block; display: block;
max-width: 76px; max-width: 76px;
margin: 5px auto 0; margin: 2px auto 0;
color: $theme-color-500; color: $theme-color-500;
font-size: 14px; font-size: 14px;
line-height: 18px; line-height: 18px;

View File

@@ -1,7 +1,5 @@
<template> <template>
<view class="container pl-12"> <view class="container pl-12">
<ModuleTitle :title="themeName" />
<view class="container-scroll font-size-0 scroll-x whitespace-nowrap"> <view class="container-scroll font-size-0 scroll-x whitespace-nowrap">
<view class="card-item bg-white inline-block rounded-20 mr-10" <view class="card-item bg-white inline-block rounded-20 mr-10"

View File

@@ -1,5 +1,5 @@
.container-scroll { .container-scroll {
margin: 4px 0 6px;
} }
.card-item { .card-item {

View File

@@ -6,9 +6,8 @@
<scroll-view class="discovery-scroll" scroll-y @scroll="handleContentScroll"> <scroll-view class="discovery-scroll" scroll-y @scroll="handleContentScroll">
<view class="discovery-body"> <view class="discovery-body">
<ExploreCards v-if="discoveryCards.length > 0 && isPanoramaGuideTab" :list="discoveryCards" <CardSwiper v-if="discoveryCards.length > 0" :list="discoveryCards" @didSelectItem="handleCardClick" />
@didSelectItem="handleCardClick" /> <ExploreCards v-if="exploreCards.length > 0" :list="exploreCards" @didSelectItem="handleCardClick" />
<CardSwiper v-else-if="discoveryCards.length > 0" :list="discoveryCards" @didSelectItem="handleCardClick" />
<QuickQuestions v-if="discoveryQuickQuestions.length > 0" :list="discoveryQuickQuestions" <QuickQuestions v-if="discoveryQuickQuestions.length > 0" :list="discoveryQuickQuestions"
@didSelectItem="handleQuickQuestionClick" /> @didSelectItem="handleQuickQuestionClick" />
@@ -47,6 +46,7 @@ const sceneId = appStore.sceneId || "";
const activeIndex = ref(0); const activeIndex = ref(0);
const discoveryTabs = ref([]); const discoveryTabs = ref([]);
const discoveryCards = ref([]); const discoveryCards = ref([]);
const exploreCards = ref([]);
const discoveryQuickQuestions = ref([]); const discoveryQuickQuestions = ref([]);
const emit = defineEmits(["content-scroll"]); const emit = defineEmits(["content-scroll"]);
@@ -106,12 +106,26 @@ const queryDiscoveryData = async (tabId) => {
/// 请求发现页卡片数据 /// 请求发现页卡片数据
const queryDiscoveryCards = async (tabId) => { const queryDiscoveryCards = async (tabId) => {
const res = await homeTabContentData({ tagId: tabId }); loadDiscoveryCards(tabId);
loadExploreCards(tabId);
}
/// 请求发现页卡片数据 轮播
const loadDiscoveryCards = async (tabId) => {
const res = await homeTabContentData({ tagId: tabId, resourceType: 0 });
if (res.code === 0) { if (res.code === 0) {
discoveryCards.value = configDataList(res.data); discoveryCards.value = configDataList(res.data);
} }
} }
/// 请求发现页探索卡片数据 大卡片
const loadExploreCards = async (tabId) => {
const res = await homeTabContentData({ tagId: tabId, resourceType: 1 });
if (res.code === 0) {
exploreCards.value = configDataList(res.data);
}
}
/// 请求快速问题列表数据 /// 请求快速问题列表数据
const queryQuickQuestionData = async (tabId) => { const queryQuickQuestionData = async (tabId) => {
const res = await homeQuickQuestionData({ tagId: tabId }); const res = await homeQuickQuestionData({ tagId: tabId });
@@ -246,5 +260,8 @@ onMounted(() => {
.discovery-body { .discovery-body {
width: 100%; width: 100%;
gap: 12px;
display: flex;
flex-direction: column;
} }
</style> </style>

View File

@@ -0,0 +1,42 @@
export const getDiscoveryPanelCollapseState = ({
currentScrollTop = 0,
lastScrollTop = 0,
isCollapsed = false,
threshold = 16,
directionThreshold = 2,
allowExpand = true,
}) => {
const current = Math.max(Number(currentScrollTop) || 0, 0);
const last = Math.max(Number(lastScrollTop) || 0, 0);
const delta = current - last;
if (delta > directionThreshold && current > threshold) {
return true;
}
if (allowExpand && delta < -directionThreshold) {
return false;
}
return isCollapsed;
};
export const canExpandDiscoveryPanelFromScroll = ({
isTouching = false,
touchDirection = "",
lastDownGestureAt = 0,
now = Date.now(),
gestureRetainMs = 800,
}) => {
if (isTouching) {
return touchDirection === "down";
}
const lastDownAt = Math.max(Number(lastDownGestureAt) || 0, 0);
if (!lastDownAt) {
return false;
}
const currentTime = Math.max(Number(now) || 0, 0);
return currentTime - lastDownAt <= gestureRetainMs;
};