22 Commits

Author SHA1 Message Date
duanshuwen
b2ad6403c1 feat: 首页动画调整 2025-11-12 18:50:40 +08:00
duanshuwen
5cb3a903ff feat: 首页动画调整 2025-11-11 21:40:14 +08:00
duanshuwen
a3087c09a8 feat: 首页侧边抽屉调整 2025-11-09 18:14:53 +08:00
duanshuwen
a7348c2002 feat: 相关商品样式调整 2025-11-09 18:01:49 +08:00
duanshuwen
921108ee6a feat: 快速预定样式调整 2025-11-09 17:42:03 +08:00
duanshuwen
84f169f792 feat: 商品详情交互调整 2025-11-09 17:35:06 +08:00
duanshuwen
bf5224dabe feat: 相关商品样式调整 2025-11-09 17:26:01 +08:00
duanshuwen
ac8baa7f95 feat: 快速预定样式修复 2025-11-09 17:09:19 +08:00
duanshuwen
0d554eeffc feat: 更多提示指令样式调整 2025-11-09 17:03:37 +08:00
duanshuwen
0eb4fa876c feat: 底部输入框调整 2025-11-09 16:52:26 +08:00
duanshuwen
6d4887aa90 feat: 相关商品交互调试 2025-11-08 14:48:26 +08:00
duanshuwen
b7a3eb39ef feat: 相关商品样式调整 2025-11-08 13:37:33 +08:00
duanshuwen
21e64ba6a6 feat: 相关商品交互调整 2025-11-08 12:45:45 +08:00
duanshuwen
213c58e5c5 feat: 调整相关商品 2025-11-07 21:15:28 +08:00
duanshuwen
0bbd5a912e feat: 相关商品问题修复 2025-11-06 21:10:26 +08:00
duanshuwen
2527cc5004 feat: 再次预定跳转商品详情问题修复 2025-11-06 18:57:45 +08:00
duanshuwen
0758179b93 feat: 问题反馈问题修复 2025-11-05 21:10:11 +08:00
duanshuwen
217ee3228a feat: 订单再次预定跳转修复 2025-11-05 20:52:37 +08:00
duanshuwen
66ff10018f feat: 呼叫服务新增房间号验证 2025-11-05 20:42:58 +08:00
duanshuwen
19c21d2510 feat: 新增首页IP交互 2025-11-05 20:34:02 +08:00
duanshuwen
fad25e951c feat: 调整首页顶部功能 2025-11-04 21:13:19 +08:00
duanshuwen
e81b7e662a feat: 首页侧边抽屉调整 2025-11-04 18:40:16 +08:00
41 changed files with 4206 additions and 2536 deletions

View File

@@ -5,7 +5,15 @@
"name": "智念",
"placeholder": "快告诉智念您在想什么~",
"loginDesc": "您好,欢迎来到智念科技",
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/dh_logo.png"
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/dh_logo.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_large.png",
"ipSmallImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_small.png",
"ipLargeImageHeight": 19687,
"ipSmallImageHeight": 3744,
"ipLargeImageStep": 147,
"ipSmallImageStep": 117,
"ipLargeTime": 4,
"ipSmallTime": 4
},
"duohua": {
"clientId": "2",
@@ -13,7 +21,15 @@
"name": "朵朵",
"placeholder": "快告诉朵朵您在想什么~",
"loginDesc": "您好,欢迎来到朵花温泉",
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/dh_logo.png"
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/dh_logo.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_large.png",
"ipSmallImage": "https://oss.nianxx.cn/mp/static/version_101/dh/dh_small.png",
"ipLargeImageHeight": 19687,
"ipSmallImageHeight": 3744,
"ipLargeImageStep": 147,
"ipSmallImageStep": 117,
"ipLargeTime": 4,
"ipSmallTime": 4
},
"tianmu": {
"clientId": "4",
@@ -21,6 +37,14 @@
"name": "沐沐",
"placeholder": "快告诉沐沐您在想什么~",
"loginDesc": "您好,欢迎来到天沐温泉",
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/tm_logo.png"
"logo": "https://oss.nianxx.cn/mp/static/version_101/login/tm_logo.png",
"ipLargeImage": "https://oss.nianxx.cn/mp/static/version_101/tm/tm_large.png",
"ipSmallImage": "https://oss.nianxx.cn/mp/static/version_101/tm/tm_small.png",
"ipLargeImageHeight": 9514,
"ipSmallImageHeight": 4736,
"ipLargeImageStep": 71,
"ipSmallImageStep": 148,
"ipLargeTime": 4,
"ipSmallTime": 6
}
}

2175
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -131,20 +131,14 @@ const workOrderId = ref(0); // 工单ID
// 处理图片上传
const handleChooseImage = () => {
console.log("选择图片");
uni.chooseImage({
count: 1,
success: (res) => {
const file = res.tempFilePaths[0];
updateImagehandle(file);
},
fail: (err) => {
console.error("选择图片失败:", err);
uni.showToast({
title: "选择图片失败",
icon: "none",
duration: 2000,
});
fail: () => {
uni.showToast({ title: "选择图片失败", icon: "none" });
},
});
};
@@ -169,21 +163,18 @@ const handleCall = async () => {
return;
}
if (!roomId.value.trim()) {
uni.showToast({ title: "请填写房间号", icon: "none" });
return;
}
if (!contactPhone.value.trim()) {
uni.showToast({
title: "请填写联系电话",
icon: "none",
duration: 2000,
});
uni.showToast({ title: "请填写联系电话", icon: "none" });
return;
}
if (!contactText.value.trim()) {
uni.showToast({
title: "请填写需求信息描述内容",
icon: "none",
duration: 2000,
});
uni.showToast({ title: "请填写需求信息描述内容", icon: "none" });
return;
}
@@ -208,31 +199,17 @@ const sendCreateWorkOrder = async () => {
// 设置成功状态
isCallSuccess.value = true;
uni.showToast({
title: "呼叫成功",
icon: "success",
duration: 2000,
});
uni.showToast({ title: "呼叫成功", icon: "success" });
} else {
uni.showToast({
title: res.message || "呼叫失败",
icon: "none",
duration: 2000,
});
uni.showToast({ title: res.message || "呼叫失败", icon: "none" });
}
} catch (error) {
console.error("呼叫失败:", error);
uni.showToast({
title: "网络错误,请重试",
icon: "none",
duration: 2000,
});
uni.showToast({ title: "网络错误,请重试", icon: "none" });
}
};
// 查看工单
const viewWorkOrder = () => {
console.log("查看工单:", workOrderId.value);
// 这里可以跳转到工单详情页面
uni.navigateTo({
url: `/pages-service/order/list`,

View File

@@ -75,20 +75,12 @@ const appName = computed(() => getCurrentConfig().name);
const handleCall = async () => {
if (!contactPhone.value.trim()) {
uni.showToast({
title: "请填写联系电话",
icon: "none",
duration: 2000,
});
uni.showToast({ title: "请填写联系电话", icon: "none" });
return;
}
if (!contactText.value.trim()) {
uni.showToast({
title: "请填写意见内容",
icon: "none",
duration: 2000,
});
uni.showToast({ title: "请填写意见内容", icon: "none" });
return;
}
@@ -110,13 +102,11 @@ const sendFeedback = async () => {
uni.showToast({
title: "反馈意见成功",
icon: "success",
duration: 2000,
});
} else {
uni.showToast({
title: res.message || "反馈意见失败",
icon: "none",
duration: 2000,
});
}
} catch (error) {
@@ -124,7 +114,6 @@ const sendFeedback = async () => {
uni.showToast({
title: "网络错误,请重试",
icon: "none",
duration: 2000,
});
}
};

View File

@@ -1,6 +1,9 @@
<template>
<view class="mt-16">
<view v-if="goodsData.commodityPurchaseInstruction">
<view class="border-box border-top-8">
<view
v-if="goodsData.commodityPurchaseInstruction"
class="border-box pl-12 pr-12"
>
<ModuleTitle
v-if="showTitle"
:title="goodsData.commodityPurchaseInstruction.templateTitle"

View File

@@ -1,7 +1,7 @@
.store-address {
display: flex;
align-items: center;
margin: 6px 0;
margin: 6px 12px;
padding: 16px 12px;
background-image: url("./images/loc_bg_img.png"); // 预留背景图片位置,用户手动导入
background-size: cover;

View File

@@ -4,5 +4,5 @@
}
.stepper-text {
width: 30px;
max-width: 40px;
}

View File

@@ -1,155 +1,245 @@
<template>
<view class="swipe-cards" @touchmove.stop>
<view class="card border-box pb-12 relative mt-12">
<view
v-for="(card, index) in visibleCards"
:key="`${card.commodityId}-${index}`"
class="card"
:style="cardStyle(index, card)"
@touchstart="touchStart($event, index)"
@touchmove="touchMove($event, index)"
@touchend="touchEnd($event, index)"
class="card-item absolute overflow-hidden"
v-for="(card, index) in list"
:key="card.__uid"
:style="[itemStyle(index, card), transformStyle(index, card)]"
@touchstart.stop="touchStart($event, index)"
@touchmove.stop.prevent="touchMove($event, index)"
@touchend.stop="touchEnd(index)"
@touchcancel.stop="touchCancel(index)"
@transitionend="onTransitionEnd(index)"
>
<slot :card="card" />
<view class="inner-card bg-white">
<!-- 商品大图部分自适应剩余空间 -->
<view class="goods-image-wrapper relative">
<image class="w-full h-full" :src="card.commodityPhoto" />
<view
class="goods-title absolute left-0 right-0 bottom-0 border-box p-12"
>
<view class="font-size-14 font-500 color-white ellipsis-1">
{{ card.commodityName }}
</view>
<view class="card-price-row color-white">
<text class="font-size-11"></text>
<text class="font-size-14 font-bold">
{{ card.specificationPrice }}
</text>
<text class="font-size-11 ml-2" v-if="card.stockUnitLabel"
>/{{ card.stockUnitLabel }}</text
>
</view>
</view>
</view>
<!-- 底部相册部分固定比例或高度 -->
<view class="border-box p-12 flex flex-justify-between">
<view
v-for="(item, index) in card.commodityPhotoList"
:key="index"
class="album-item relative overflow-hidden bg-f5 rounded-10"
>
<image :src="item.photoUrl" mode="aspectFill" />
<view
class="album-title absolute left-0 right-0 bottom-0 color-white font-size-11 font-500 ellipsis-1 flex flex-items-center flex-justify-center"
>
{{ item.photoName }}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from "vue";
import { ref, watch } from "vue";
import { checkToken } from "@/hooks/useGoLogin";
const props = defineProps({
cardsData: Array,
threshold: { type: Number, default: 100 },
cardsData: { type: Array, default: () => [] },
});
const cards = ref([...props.cardsData]);
const startX = ref(0);
const startY = ref(0);
const moveX = ref(0);
const moveY = ref(0);
const currentIndex = ref(null);
const isHorizontal = ref(false); // 是否为水平滑动
const DURATION = 300;
const CLICK_THRESHOLD = 8; // 点击判定的最大偏移阈值(像素)
const swipering = ref(false);
const animatingOut = ref(false);
let reorderTimer = null;
const visibleCards = computed(() => cards.value.slice(0, 3));
const { windowWidth } = uni.getWindowInfo();
function touchStart(e, index) {
const touch = e.touches[0];
startX.value = touch.clientX;
startY.value = touch.clientY;
currentIndex.value = index;
isHorizontal.value = false;
}
let uidCounter = 0;
// 始终生成全局唯一的 __uid避免因重复 key 导致后续卡片无法正确重渲染与绑定事件
const genUid = (item) =>
`swipe_${item?.commodityId ?? "unknown"}_${uidCounter++}_${Date.now()}`;
const normalize = (item) => ({
...item,
__uid: genUid(item),
x: 0,
y: 0,
opacity: 1,
});
function touchMove(e, index) {
if (currentIndex.value !== index) return;
const touch = e.touches[0];
moveX.value = touch.clientX - startX.value;
moveY.value = touch.clientY - startY.value;
// 循环队列全量堆栈仅前3张
const queue = ref((props.cardsData || []).map(normalize));
const list = ref(queue.value.slice(0, 3));
const updateStack = () => {
list.value = queue.value.slice(0, 3);
};
// 第一次移动时判断方向
if (
!isHorizontal.value &&
(Math.abs(moveX.value) > 10 || Math.abs(moveY.value) > 10)
) {
if (Math.abs(moveX.value) > Math.abs(moveY.value)) {
isHorizontal.value = true;
watch(
() => props.cardsData,
(val) => {
queue.value = (val || []).map(normalize);
updateStack();
},
{ deep: true }
);
// 触摸状态
const touchState = ref({
startX: 0,
startY: 0,
moving: false,
isClickCandidate: false,
});
const touchStart = (e, index) => {
if (index !== 0 || animatingOut.value) return;
const t = e.changedTouches?.[0];
if (!t) return;
touchState.value.startX = t.clientX;
touchState.value.startY = t.clientY;
touchState.value.moving = true;
// 初始认为可能是点击,移动过程中如果超过阈值则取消点击
touchState.value.isClickCandidate = true;
swipering.value = true;
};
const touchMove = (e, index) => {
if (index !== 0 || !touchState.value.moving || animatingOut.value) return;
const t = e.changedTouches?.[0];
if (!t) return;
const dx = t.clientX - touchState.value.startX;
const dy = t.clientY - touchState.value.startY;
// 超过点击阈值则标记为不是点击
if (Math.abs(dx) > CLICK_THRESHOLD || Math.abs(dy) > CLICK_THRESHOLD) {
touchState.value.isClickCandidate = false;
}
const top = list.value[0];
if (!top) return;
top.x = dx;
top.y = dy;
};
const finalizeReorder = () => {
const top = queue.value[0];
if (!top) return;
const moved = { ...top, x: 0, y: 0, opacity: 1 };
queue.value = [...queue.value.slice(1), moved];
updateStack();
animatingOut.value = false;
if (reorderTimer) {
clearTimeout(reorderTimer);
reorderTimer = null;
}
};
const touchEnd = (index) => {
if (index !== 0 || !touchState.value.moving) return;
touchState.value.moving = false;
swipering.value = false;
const top = list.value[0];
if (!top) return;
// 若在有效点击范围内,则触发跳转,不进行滑动逻辑
if (touchState.value.isClickCandidate) {
top.x = 0;
top.y = 0;
top.opacity = 1;
uni.navigateTo({
url: `/pages/goods/index?commodityId=${top.commodityId}`,
});
return;
}
const threshold = windowWidth / 4;
if (Math.abs(top.x) > threshold) {
const direction = top.x > 0 ? 1 : -1;
animatingOut.value = true;
top.x = direction * (windowWidth + 100);
top.opacity = 0;
if (reorderTimer) {
clearTimeout(reorderTimer);
reorderTimer = null;
}
}
// 如果是水平滑动,阻止页面滚动
if (isHorizontal.value) {
e.preventDefault?.();
}
}
function touchEnd(e, index) {
if (!isHorizontal.value) return; // 如果不是水平滑动,不处理
if (Math.abs(moveX.value) > props.threshold) {
const direction = moveX.value > 0 ? 1 : -1;
animateSwipe(index, direction);
reorderTimer = setTimeout(() => {
if (animatingOut.value) finalizeReorder();
}, DURATION + 40);
} else {
resetCard(index);
// 回弹复位
top.x = 0;
top.y = 0;
top.opacity = 1;
}
moveX.value = 0;
moveY.value = 0;
currentIndex.value = null;
}
};
function animateSwipe(index, direction) {
const card = cards.value[index];
if (!card) return;
card.swiped = true;
card.direction = direction;
setTimeout(async () => {
const removed = cards.value.splice(index, 1)[0];
removed.swiped = false;
removed.direction = 0;
cards.value.push(removed);
await nextTick();
}, 300);
}
const touchCancel = (index) => {
if (index !== 0) return;
const top = list.value[0];
if (!top) return;
touchState.value.moving = false;
swipering.value = false;
top.x = 0;
top.y = 0;
top.opacity = 1;
};
function resetCard(index) {
const card = cards.value[index];
if (!card) return;
card.swiped = false;
card.direction = 0;
}
const onTransitionEnd = (index) => {
if (index !== 0) return;
if (!animatingOut.value) return;
finalizeReorder();
};
function cardStyle(index, card) {
const baseIndex = visibleCards.value.length - index;
const zIndex = baseIndex;
const scale = 1 - index * 0.05;
const translateY = index * 28;
// 栈样式:层级、基础位移缩放、过渡时长
const itemStyle = (index, card) => {
const zIndex = list.value.length - index;
const duration = swipering.value ? "0ms" : `${DURATION}ms`;
const opacity = card.opacity;
return {
zIndex,
opacity,
transition: `transform ${duration} ease, opacity ${duration} ease`,
};
};
const translateDrag =
currentIndex.value === index
? `translate(${moveX.value}px, ${moveY.value}px)`
: `translate(0, ${translateY}rpx)`;
const rotate =
currentIndex.value === index
? `rotate(${moveX.value / 15}deg)`
: "rotate(0)";
let opacity = 1;
if (card.swiped) opacity = 0;
let transform = `${translateDrag} scale(${scale}) ${rotate}`;
if (card.swiped) {
const x = card.direction * 500;
transform = `translate(${x}px, ${moveY.value}px) rotate(${
card.direction * 45
}deg)`;
}
return `
z-index: ${zIndex};
background: ${card.color};
transform: ${transform};
opacity: ${opacity};
transition: ${
card.swiped ? "all 0.3s ease-out" : "transform 0.2s ease-out"
// 变换样式:顶部卡动态位移/旋转,后续卡预览层级
const transformStyle = (index, card) => {
if (index === 0) {
const deg = card.x / 20;
return {
transform: `translate3d(${card.x}px, ${card.y}px, 0) rotate(${deg}deg)`,
};
`;
}
}
// 预览层:轻微位移与缩放,确保连贯顶上
const previewScales = [1, 0.94, 0.86];
const previewOffsets = [0, 18, 39];
const scale = previewScales[index] ?? 0.94;
const y = previewOffsets[index] ?? 24;
return {
transform: `translate3d(0, ${y}px, 0) scale(${scale})`,
};
};
// 去下单
const placeOrderHandle = (item) => {
console.log("去下单", item);
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
});
};
</script>
<style scoped lang="scss">
.swipe-cards {
position: relative;
height: 320px;
display: flex;
justify-content: center;
padding: 6px 6px 12px 6px;
}
.card {
position: absolute;
width: calc(100% - 12px);
height: calc(100% - 30px);
border-radius: 20px;
box-shadow: 0 8px 8px rgba(0, 0, 0, 0.08);
background: #fff;
}
</style>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,40 @@
.card {
height: 308px;
}
.card-item {
inset: 0;
will-change: transform, opacity;
height: 277px;
box-shadow: 0 8px 8px rgba(0, 0, 0, 0.08);
border-radius: 20px;
}
.inner-card {
width: 100%;
height: 100%;
}
/* 商品大图部分:撑满除相册外的空间 */
.goods-image-wrapper {
width: 100%;
height: 193px;
}
.goods-title {
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.6) 100%
);
}
.album-item {
width: 96px;
height: 60px;
}
.album-title {
background: rgba(0, 0, 0, 0.5);
height: 20px;
}

View File

@@ -11,7 +11,9 @@
</template>
</TopNavBar>
<view class="booking-content flex-full border-box p-12">
<view
class="booking-content flex-full border-box p-12 overflow-hidden scroll-y"
>
<!-- 预约内容 -->
<view class="border-box bg-white p-12 rounded-12 mb-12">
<!-- 酒店类型入住离店日期部分 -->

View File

@@ -17,7 +17,7 @@
'bg-2D91FF': ['1', '2', '3', '4', '5', '6'].includes(statusCode),
},
]"
@click="handleButtonClick"
@click="handleButtonClick(orderData)"
>
{{ buttonText }}
</button>
@@ -67,7 +67,7 @@ const emit = defineEmits(["refund", "refresh"]);
const handleButtonClick = async (orderData) => {
try {
// 再次预定跳转商品详情
if (["3", "5", "6"].includes(statusCode.value)) {
if (["1", "2", "3", "4", "5", "6"].includes(statusCode.value)) {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${orderData.commodityId}`,
});

View File

@@ -14,7 +14,7 @@
</view>
<view
v-if="item.commodityFacility"
class="font-size-12 line-height-16 color-99A0AE mb-4"
class="font-size-12 line-height-16 color-99A0AE mb-4 ellipsis-1"
>
{{ item.commodityFacility.join(" ") }}
</view>

View File

@@ -4,6 +4,7 @@
}
.right {
max-width: 259px;
height: 100%;
}

View File

@@ -1,26 +1,33 @@
<template>
<view class="mt-16">
<view class="mt-16 border-box border-top-8">
<view
class="border-box pt-12 pl-12 pr-12"
v-for="(moduleItem, index) in goodsData.commodityEquipment"
:key="index"
>
<view
class="flex flex-items-start flex-col pt-12 pb-12"
class="flex flex-items-start flex-col"
:class="{
'border-bottom': index < goodsData.commodityEquipment.length - 1,
}"
>
<view class="flex flex-items-center flex-row flex-shrink-0 mr-8">
<uni-icons fontFamily="znicons" size="20" color="#171717">{{
zniconsMap[moduleItem.icon]
}}</uni-icons>
<text class="font-size-12 color-171717 line-height-20">{{
moduleItem.title
}}</text>
<view class="flex flex-items-center flex-row flex-shrink-0">
<uni-icons fontFamily="znicons" size="20" color="#171717">
{{ zniconsMap[moduleItem.icon] }}
</uni-icons>
<text class="font-size-12 color-171717 line-height-20">
{{ moduleItem.title }}
</text>
</view>
<view class="border-box flex flex-items-center flex-row mt-4 pb-12">
<text
class="font-size-12 color-525866 line-height-20 mr-4"
v-for="(text, index) in moduleItem.desc"
:key="index"
>
{{ text }}
</text>
</view>
<text class="flex-full font-size-12 color-525866 line-height-20 mt-4">{{
moduleItem.desc
}}</text>
</view>
</view>
</view>

View File

@@ -1,5 +1,5 @@
<template>
<view class="good-info">
<view class="good-info border-box pl-12 pr-12">
<!-- 标题区域 -->
<view class="title-section">
<text class="title">

View File

@@ -1,8 +1,8 @@
<template>
<view class="goods-container bg-gray">
<TopNavBar
title="商品详情"
:backgroundColor="`rgba(217, 238, 255, ${navOpacity})`"
:title="navOpacity < 0.5 ? '' : '商品详情'"
:background="`rgba(217, 238, 255, ${navOpacity})`"
:titleColor="navOpacity < 0.5 ? '#ffffff' : '#000000'"
:backIconColor="navOpacity < 0.5 ? '#ffffff' : '#000000'"
/>

View File

@@ -68,7 +68,7 @@ $button-color: #00a6ff;
.goods-content {
border-radius: 28px 28px 0 0;
background-color: #fff;
padding: 20px 12px;
padding: 20px 0;
position: relative;
margin-top: -30px;
z-index: 1;

View File

@@ -36,13 +36,13 @@
</template>
<script setup>
import { ref, onMounted, defineEmits } from "vue";
import { ref, defineEmits, defineExpose } from "vue";
import { getLoginUserPhone } from "@/request/api/LoginApi";
import { NOTICE_EVENT_LOGOUT } from "@/constant/constant";
import { useAppStore } from "@/store";
const appStore = useAppStore();
const emits = defineEmits(["closeDrawer"]);
const emits = defineEmits(["close"]);
//
const userInfo = ref({
@@ -64,11 +64,6 @@ const menuList = ref([
// { label: "", type: "action", action: "subscribeMessage" },
]);
//
onMounted(() => {
getLoginUserPhoneInfo();
});
const getLoginUserPhoneInfo = async () => {
const res = await getLoginUserPhone();
@@ -107,12 +102,14 @@ const handleLogout = () => {
uni.clearStorageSync();
appStore.setHasToken(false);
appStore.setTokenExpired(true);
emits("closeDrawer");
emits("close");
uni.$emit(NOTICE_EVENT_LOGOUT);
}
},
});
};
defineExpose({ getLoginUserPhoneInfo });
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,47 @@
<template>
<uni-drawer ref="drawerRef" mode="left" :width="320">
<view class="drawer-home">
<view class="drawer-home-nav">
<uni-icons
type="closeempty"
size="22"
color="#333333"
class="close-icon"
@click="close"
/>
<text class="title">我的</text>
</view>
<MineSetting ref="mineSettingRef" @close="close" />
</view>
</uni-drawer>
</template>
<script setup>
import { ref, defineExpose } from "vue";
import { checkToken } from "@/hooks/useGoLogin";
import MineSetting from "./components/MineSetting/index.vue";
const drawerRef = ref(null);
// 监听抽屉显示事件
const mineSettingRef = ref(null);
const open = () => {
checkToken().then(async () => {
await mineSettingRef.value.getLoginUserPhoneInfo();
drawerRef.value.open();
});
};
// 监听抽屉隐藏事件
const close = () => drawerRef.value.close();
defineExpose({
open,
close,
});
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -63,12 +63,12 @@
</view>
</view>
<!-- 录音按钮 -->
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
<view class="color-99A0AE font-size-9 text-center text-gray-400">
内容由AI大模型生成请仔细鉴别
</view>
<!-- 录音按钮 -->
<RecordingWaveBtn v-if="visibleWaveBtn" ref="recordingWaveBtnRef" />
</view>
</template>

View File

@@ -1,8 +1,11 @@
<template>
<view class="flex flex-col h-screen" @touchend="handleTouchEnd">
<view class="flex flex-col h-screen">
<!-- 顶部自定义导航栏 -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
<ChatTopNavBar @openDrawer="openDrawer" />
<ChatTopNavBar
ref="topNavBarRef"
:mainPageDataModel="mainPageDataModel"
/>
</view>
<!-- 消息列表可滚动区域 -->
@@ -15,7 +18,7 @@
@scrolltolower="handleScrollToLower"
>
<!-- welcome栏 -->
<ChatTopWelcome :mainPageDataModel="mainPageDataModel" />
<ChatTopWelcome ref="welcomeRef" :mainPageDataModel="mainPageDataModel" />
<view
class="area-msg-list-content"
@@ -120,7 +123,7 @@
</template>
<script setup>
import { onMounted, nextTick, onUnmounted, ref, defineEmits } from "vue";
import { onMounted, nextTick, onUnmounted, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import {
SCROLL_TO_BOTTOM,
@@ -153,7 +156,7 @@ import {
recentConversation,
} from "@/request/api/ConversationApi";
import WebSocketManager from "@/utils/WebSocketManager";
import { IdUtils } from "@/utils";
import { ThrottleUtils, IdUtils } from "@/utils";
import { checkToken } from "@/hooks/useGoLogin";
import { useAppStore } from "@/store";
@@ -162,6 +165,8 @@ const appStore = useAppStore();
const statusBarHeight = ref(20);
/// 输入框组件引用
const inputAreaRef = ref(null);
const topNavBarRef = ref();
const welcomeRef = ref();
const holdKeyboardTimer = ref(null);
/// focus时点击页面的时候不收起键盘
@@ -174,9 +179,6 @@ const isKeyboardShow = ref(false);
///(控制滚动位置)
const scrollTop = ref(99999);
// 用户滚动状态控制
const isUserScrolling = ref(false);
/// 会话列表
const chatMsgList = ref([]);
/// 输入口的输入消息
@@ -202,10 +204,6 @@ let webSocketConnectStatus = false;
// 当前会话的消息ID用于保持发送和终止的messageId一致
let currentSessionMessageId = null;
// 打开抽屉
const emits = defineEmits(["openDrawer"]);
const openDrawer = () => emits("openDrawer");
/// =============事件函数↓================
const handleTouchEnd = () => {
clearTimeout(holdKeyboardTimer.value);
@@ -238,9 +236,10 @@ const handleKeyboardHide = () => {
};
// 处理用户滚动事件
const handleScroll = (e) => {
// 标记用户正在滚动
};
const welcomeHeight = ref(0);
const handleScroll = ThrottleUtils.createThrottle(({ detail }) => {
topNavBarRef.value.show = parseInt(detail.scrollTop) > welcomeHeight.value;
}, 50);
// 处理滚动到底部事件
const handleScrollToLower = () => {};
@@ -385,6 +384,7 @@ const loadConversationMsgList = async () => {
};
// 获取首页数据
const getMainPageData = async () => {
/// 从个渠道获取如二维,没有的时候就返回首页的数据
const sceneId = appStore.sceneId || "";
@@ -394,6 +394,11 @@ const getMainPageData = async () => {
initData();
mainPageDataModel.value = res.data;
agentId.value = res.data.agentId;
// 数据更新后再次测量欢迎区高度
welcomeRef.value.measureWelcomeHeight((data) => {
console.log("🚀 ~ getMainPageData ~ data:", data);
welcomeHeight.value = data.height;
});
}
appStore.setSceneId(""); // 清空sceneId,分身二次进入展示默认的
};

View File

@@ -7,7 +7,7 @@
:key="index"
>
<text
class="font-500 font-size-12 line-height-24 text-center"
:class="['font-500 font-size-12 text-center', `color-${index}`]"
@click="sendReply(item)"
>
{{ item }}

View File

@@ -6,6 +6,7 @@
.more-tips-item {
background: #f7f7f7;
border-radius: 5px;
box-sizing: border-box;
padding: 6px 8px;
&:last-child {
@@ -13,3 +14,31 @@
}
}
}
.color-0 {
color: #47c2ff;
}
.color-1 {
color: #fb3748;
}
.color-2 {
color: #1fc16b;
}
.color-3 {
color: #f6b51e;
}
.color-4 {
color: #7d52f4;
}
.color-5 {
color: #fb4ba3;
}
.color-6 {
color: #22d3bb;
}

View File

@@ -1,35 +1,57 @@
<template>
<view class="nav-bar">
<view class="nav-item" @click="showDrawer('showLeft')">
<uni-icons type="bars" size="24" color="#333"></uni-icons>
<view class="border-box h-44 flex flex-items-center pl-12 pr-12">
<uni-icons type="bars" size="24" color="#333" @click="showDrawer" />
<!-- 隐藏 -->
<view class="flex-full h-full flex flex-items-center flex-justify-center">
<!-- ChatTopWelcome不在可视区显示并添加动画在可视区隐藏 -->
<view
v-show="show"
:class="['w-32 h-32', { 'image-animated': show }]"
:style="getStyle"
></view>
<text
v-show="show"
class="font-size-14 font-500 color-171717 ml-10"
:class="{ 'text-animated': show }"
>沐沐</text
>
</view>
<uni-drawer ref="showLeft" mode="left" :width="320">
<DrawerHome @closeDrawer="closeDrawer('showLeft')" />
</uni-drawer>
<view class="w-24 h-24"></view>
</view>
</template>
<script setup>
import { ref } from "vue";
import DrawerHome from "../../drawer/DrawerHome/index.vue";
import { checkToken } from "@/hooks/useGoLogin";
const showLeft = ref(false);
import { ref, defineProps, computed, defineExpose } from "vue";
import { getCurrentConfig } from "@/constant/base";
// 打开窗口
const showDrawer = async (e) => {
await checkToken();
const props = defineProps({
mainPageDataModel: {
type: Object,
default: () => ({}),
},
});
showLeft.value.open();
// 发送抽屉显示事件
uni.$emit("drawerShow");
};
// 关闭窗口
const closeDrawer = (e) => {
showLeft.value.close();
// 发送抽屉隐藏事件
uni.$emit("drawerHide");
};
const show = ref(false);
const getStyle = computed(() => {
const config = getCurrentConfig();
return {
"--ipSmallImageStep": config.ipSmallImageStep,
"--ipSmallImageHeight": config.ipSmallImageHeight,
"--ipSmallTime": config.ipSmallTime,
backgroundImage: `url(${config.ipSmallImage})`,
backgroundRepeat: "no-repeat",
backgroundSize: "32px auto",
backgroundPosition: "0 0",
};
});
const showDrawer = () => uni.$emit("SHOW_DRAWER");
defineExpose({ show });
</script>
<style lang="scss" scoped>

View File

@@ -1,16 +1,42 @@
.nav-bar {
display: flex;
align-items: center;
height: 44px;
padding: 0 15px;
// 图片从0%到100%动画
.image-animated {
animation: logo-scale 0.3s ease-in-out,
sprite-play calc(var(--ipSmallTime) * 1s) steps(var(--ipSmallImageStep))
infinite;
}
.nav-item {
width: 24px;
height: 24px;
margin-right: 10px;
@keyframes logo-scale {
0% {
transform: scale(0);
}
.nav-item-icon {
width: 100%;
height: 100%;
100% {
transform: scale(1);
}
}
@keyframes sprite-play {
0% {
background-position: 0 0;
}
100% {
/* 117 帧 × 每帧高度约 32px终点应为 -(117-1)*32 = -5421px */
background-position: 0 calc(var(--ipSmallImageHeight) * -1px);
}
}
// 文字从0%到100%动画,从左到右
.text-animated {
animation: text-fade-in 0.3s ease-in-out;
}
@keyframes text-fade-in {
0% {
opacity: 0;
transform: translateX(-20px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}

View File

@@ -1,14 +1,11 @@
v
<template>
<view class="welcome-content border-box p-12">
<view class="wrap rounded-20">
<view
class="flex flex-items-center flex-justify-between border-box pl-12 pr-12"
>
<image
class="ip relative"
:src="initPageImages?.logoImageUrl"
mode="aspectFit"
/>
<view class="ip" :style="getStyle"></view>
<view
class="welcome-text font-size-14 font-500 font-family-misans-vf color-171717 line-height-24"
>
@@ -22,7 +19,8 @@
</template>
<script setup>
import { defineProps, computed } from "vue";
import { defineProps, computed, getCurrentInstance, defineExpose } from "vue";
import { getCurrentConfig } from "@/constant/base";
import ChatMoreTips from "../ChatMoreTips/index.vue";
const props = defineProps({
@@ -51,9 +49,40 @@ const props = defineProps({
},
});
const initPageImages = computed(() => props.mainPageDataModel.initPageImages);
const getStyle = computed(() => {
const config = getCurrentConfig();
return {
"--ipLargeImageStep": config.ipLargeImageStep,
"--ipLargeImageHeight": config.ipLargeImageHeight,
"--ipLargeTime": config.ipLargeTime,
backgroundImage: `url(${config.ipLargeImage})`,
backgroundRepeat: "no-repeat",
backgroundSize: "158px auto",
backgroundPosition: "0 0",
};
});
const welcomeContent = computed(() => props.mainPageDataModel.welcomeContent);
const guideWords = computed(() => props.mainPageDataModel.guideWords);
// Welcome 可视状态与高度
const instance = getCurrentInstance();
// 测量欢迎区域高度
const measureWelcomeHeight = (callback) => {
uni
.createSelectorQuery()
.in(instance)
.select(".welcome-content")
.boundingClientRect((res) => {
if (res && res.height) {
callback(res);
}
})
.exec();
};
defineExpose({ measureWelcomeHeight });
</script>
<style lang="scss" scoped>

View File

@@ -3,8 +3,20 @@
}
.ip {
flex: 0 0 123px;
width: 123px;
height: 166px;
margin-top: -20px;
flex: 0 0 158px;
width: 158px;
height: 134px;
animation: sprite-play calc(var(--ipLargeTime) * 1s)
steps(var(--ipLargeImageStep)) infinite;
}
@keyframes sprite-play {
0% {
background-position: 0 0;
}
100% {
/* 40 帧 × 每帧高度约 139px终点应为 -(40-1)*139 = -5421px */
background-position: 0 calc(var(--ipLargeImageHeight) * -1px);
}
}

View File

@@ -1,54 +0,0 @@
<template>
<view class="drawer-home">
<view class="drawer-home-nav">
<uni-icons
type="closeempty"
size="22"
color="#333333"
class="close-icon"
@click="closeDrawer"
/>
<text class="title">我的</text>
</view>
<MineSetting v-if="isDrawerVisible" @closeDrawer="closeDrawer" />
</view>
</template>
<script setup>
import { defineEmits, ref, onMounted, onUnmounted } from "vue";
import MineSetting from "../MineSetting/index.vue";
const emits = defineEmits(["closeDrawer"]);
const isDrawerVisible = ref(false);
const closeDrawer = () => {
console.log("关闭抽屉");
isDrawerVisible.value = false;
emits("closeDrawer");
};
// 监听抽屉显示事件
const handleDrawerShow = () => {
isDrawerVisible.value = true;
};
// 监听抽屉隐藏事件
const handleDrawerHide = () => {
isDrawerVisible.value = false;
};
onMounted(() => {
uni.$on("drawerShow", handleDrawerShow);
uni.$on("drawerHide", handleDrawerHide);
});
onUnmounted(() => {
uni.$off("drawerShow", handleDrawerShow);
uni.$off("drawerHide", handleDrawerHide);
});
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -5,7 +5,9 @@
}
.tag-item {
background-color: $uni-bg-color;
box-sizing: border-box;
border: 1px solid #fff;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
padding: 4px 10px;
margin-right: 8px;

View File

@@ -2,54 +2,12 @@
<view class="container">
<ModuleTitle title="相关商品" />
<SwipeCards :cardsData="commodityList">
<template #default="{ card }">
<view class="inner-card" @click="placeOrderHandle(card)">
<!-- 商品大图部分自适应剩余空间 -->
<view class="goods-image-wrapper">
<image
class="goods-image"
:src="card.commodityPhoto"
mode="aspectFill"
></image>
<view class="goods-title">
<text class="goods-text">{{ card.commodityName }}</text>
<view class="card-price-row">
<text class="card-price-fu"></text>
<text class="card-price">{{ card.specificationPrice }}</text>
<text class="card-unit" v-if="card.stockUnitLabel"
>/{{ card.stockUnitLabel }}</text
>
</view>
</view>
</view>
<!-- 底部相册部分固定比例或高度 -->
<view class="album-row">
<view
v-for="(item, index) in normalizedAlbums(commodityList)"
:key="index"
class="album-item"
>
<view v-if="item" class="album-image-wrapper">
<image
class="album-image"
:src="item.commodityPhoto"
mode="aspectFill"
/>
<view class="album-title">{{ item.commodityName }}</view>
</view>
</view>
</view>
</view>
</template>
</SwipeCards>
<SwipeCards :cardsData="commodityList" />
</view>
</template>
<script setup>
import { defineProps } from "vue";
import { checkToken } from "@/hooks/useGoLogin";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
import SwipeCards from "@/components/SwipeCards/index.vue";
@@ -59,136 +17,8 @@ const props = defineProps({
default: [],
},
});
// 补齐3个的
const normalizedAlbums = (list = []) => {
const arr = list.slice(0, 3);
while (arr.length < 3) arr.push(null);
return arr;
};
// 去下单
const placeOrderHandle = (item) => {
checkToken().then(() => {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
});
});
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
.inner-card {
width: 100%;
height: 100%;
background: #fff;
border-radius: 20px;
flex-direction: column;
// overflow: hidden;
}
/* 商品大图部分:撑满除相册外的空间 */
.goods-image-wrapper {
position: relative;
width: 100%;
aspect-ratio: 335 / 200;
overflow: hidden;
}
.goods-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.goods-title {
position: absolute;
inset: 0;
padding: 12px;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.6) 100%
);
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.goods-text {
color: #fff;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-price-fu {
color: #fff;
font-size: 11px;
font-weight: normal;
}
.card-price {
color: #fff;
font-size: $uni-font-size-lg;
font-weight: bold;
}
.card-unit {
font-size: 11px;
color: #fff;
font-weight: normal;
margin-left: 2px;
}
.album-row {
display: flex;
justify-content: space-between;
padding: 12px;
box-sizing: border-box;
flex-shrink: 0;
aspect-ratio: 335 / 84;
}
.album-item {
width: calc((100% - 24px) / 3);
height: 100%;
border-radius: 10px;
overflow: hidden;
background: #f5f5f5;
}
.album-image-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.album-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.album-title {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.6) 100%
);
color: #fff;
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

View File

@@ -14,6 +14,9 @@
<!-- 更多服务 -->
<MoreService />
<!-- 抽屉组件 -->
<DrawerSection ref="drawerRef" @close="closeDrawer" />
</template>
<script setup>
@@ -21,9 +24,11 @@ import { onLoad } from "@dcloudio/uni-app";
import { ref, onUnmounted } from "vue";
import { getUrlParams } from "@/utils/UrlParams";
import { useAppStore } from "@/store";
import { checkToken } from "@/hooks/useGoLogin";
import ChatMainList from "./components/chat/ChatMainList/index.vue";
import Calender from "@/components/Calender/index.vue";
import MoreService from "./components/module/MoreService/index.vue";
import DrawerSection from "./components/DrawerSection/index.vue";
const appStore = useAppStore();
@@ -56,6 +61,19 @@ const getWeixinMiniProgramParams = (e) => {
}
};
// 打开窗口
const drawerRef = ref(null);
const showDrawer = async (e) => {
await checkToken();
drawerRef.value.open();
};
uni.$on("SHOW_DRAWER", showDrawer);
// 关闭窗口
const closeDrawer = (e) => drawerRef.value.close();
onLoad((e) => {
getWeixinMiniProgramParams(e);
});

View File

@@ -7,6 +7,10 @@
border-top: 1px solid #e5e8ee;
}
.border-top-8 {
border-top: 8px solid #f5f5f5;
}
.border-bottom {
border-bottom: 1px solid #e5e8ee;
}

View File

@@ -0,0 +1,3 @@
.border-box {
box-sizing: border-box;
}

View File

@@ -9,3 +9,15 @@
.h-80 {
height: 80px;
}
.h-44 {
height: 44px;
}
.h-32 {
height: 32px;
}
.h-24 {
height: 24px;
}

View File

@@ -19,3 +19,4 @@
@import "./width.scss";
@import "./z-index.scss";
@import "./white-space.scss";
@import "./box-sizing.scss";

View File

@@ -59,6 +59,10 @@
margin-bottom: 10px;
}
.ml-10 {
margin-left: 10px;
}
.m-12 {
margin: 12px;
}

View File

@@ -6,3 +6,15 @@
.absolute {
position: absolute;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.bottom-0 {
bottom: 0;
}

View File

@@ -6,6 +6,14 @@
width: 100vw;
}
.w-24 {
width: 24px;
}
.w-32 {
width: 32px;
}
.w-50 {
width: 50%;
}

3423
yarn.lock

File diff suppressed because it is too large Load Diff