feat: 调整了目录结构

This commit is contained in:
2026-04-23 16:37:26 +08:00
parent 8161e7512b
commit 736c2feb4f
58 changed files with 2370 additions and 373 deletions

View File

@@ -0,0 +1,55 @@
<template>
<view class="container">
<swiper
class="swiper"
circular
:indicator-dots="activityList.length > 1"
indicator-color="#FFFFFF"
indicator-active-color="#00A6FF"
:autoplay="autoplay"
:interval="interval"
:duration="duration"
>
<swiper-item v-for="item in activityList" :key="item.id">
<view class="swiper-item" @click="handleClick()">
<image
class="swiper-img"
:src="item.activityCover"
mode="aspectFill"
></image>
<view class="corner-btn">{{ commandModel.title }}</view>
</view>
</swiper-item>
</swiper>
</view>
</template>
<script setup>
import { ref } from "vue";
import { SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
const commandModel = ref({
icon: "",
title: "快速预定",
type: Command.quickBooking,
});
const autoplay = ref(true);
const interval = ref(3000);
const duration = ref(500);
const props = defineProps({
activityList: {
type: Array,
default: [],
},
});
const handleClick = () => {
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, commandModel.value);
};
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,38 @@
.container {
margin-bottom: 6px;
.uni-margin-wrap {
width: 100%;
}
.swiper {
height: 120px;
border-radius: 8px;
}
.swiper-item {
position: relative;
display: block;
height: 120px;
line-height: 120px;
text-align: center;
.swiper-img {
width: 100%;
height: 100%;
border-radius: 8px;
display: block;
}
.corner-btn {
position: absolute;
right: 12px;
bottom: 12px;
background-color: #ffeb00;
color: $uni-text-color;
font-size: $uni-font-size-base;
font-weight: 500;
padding: 4px 12px;
border-radius: 20px;
line-height: 1.5;
}
}
}

View File

@@ -0,0 +1,150 @@
<template>
<view class="w-full bg-white border-box border-ff overflow-hidden rounded-20 flex flex-col">
<!-- 占位撑开 -->
<view class="w-vw"></view>
<view class="flex flex-col p-16 border-box border-left-4">
<view v-if="title" class="flex flex-row flex-items-start flex-justify-start mb-8">
<uni-icons class="icon-active" type="fire-filled" size="18" color="opacity" />
<text class="font-size-16 font-500 text-color-900 ml-6"> {{ title }}</text>
</view>
<!-- 文字内容最多显示3行 -->
<view class="answer-content font-size-12 font-color-600">
<ChatMarkdown :key="textKey" :text="processedText" />
</view>
<!-- 超过3行时显示...提示 -->
<view v-if="!finish" class="flex flex-row flex-items-center mt-8">
<text class="font-size-12 font-400 font-color-600">正在生成</text>
<ChatLoading />
</view>
<view v-if="isOverflow" class="flex flex-row flex-items-center mt-8" @click="lookDetailAction">
<text class="font-size-12 font-400 theme-color-500 mr-4">查看详情</text>
<uni-icons class="icon-active" type="right" size="14" color="opacity"></uni-icons>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps, computed, ref, watch, onBeforeUnmount } from "vue";
import ChatMarkdown from "../../ChatMain/ChatMarkdown/index.vue";
import ChatLoading from "../../ChatMain/ChatLoading/index.vue";
import StreamManager from '@/utils/StreamManager.js';
const isOverflow = ref(false)
// 直接根据文字长度判断超过约100个字符认为会溢出约3行
const props = defineProps({
title: {
type: String,
default: "",
},
text: {
type: String,
default: "",
},
finish: {
type: Boolean,
default: false,
},
});
// 用于强制重新渲染的key
const textKey = ref(0);
// 处理文本内容:按行截断以保证预览最多显示三行(更贴近视觉行数)
// 点击“查看详情”会跳转到完整页面(不受预览截断影响)。
const PREVIEW_LINES = 3;
const PREVIEW_CHAR_LIMIT = 100; // 作为备用,当没有换行但过长时也会截断
const processedText = computed(() => {
const txt = props.text ? String(props.text) : "";
if (!txt) return "";
// 按行分割(保留空行)
const lines = txt.split(/\r?\n/);
// 如果行数超过限制,截取前 PREVIEW_LINES 行并添加省略号
if (lines.length > PREVIEW_LINES) {
return lines.slice(0, PREVIEW_LINES).join("\n") + "...";
}
// 若虽然行数不超过,但总长度仍然很长,做字符级截断作为兜底
if (txt.length > PREVIEW_CHAR_LIMIT) {
return txt.slice(0, PREVIEW_CHAR_LIMIT) + "...";
}
return txt;
});
// 监听 text 变化:更新 textKey 并同步 isOverflow合并为单一响应函数避免冗余
watch(
() => props.text,
(newText, oldText) => {
const textStr = newText ? String(newText) : "";
const lines = textStr.split(/\r?\n/);
isOverflow.value = lines.length > PREVIEW_LINES || textStr.length > PREVIEW_CHAR_LIMIT;
if (newText !== oldText) {
textKey.value++;
}
},
{ immediate: true }
);
const lookDetailAction = () => {
const message = props.text ? String(props.text) : "";
// 使用 StreamManager 以 streamId 转发当前及后续流式更新,详情页通过 streamId 订阅
const streamId = `stream_${Date.now()}_${Math.random().toString(36).slice(2,8)}`;
StreamManager.openStream(streamId, message, !!props.finish);
// 将当前组件后续 props.text/props.finish 的更新转发到 StreamManager
const stopForward = watch(
() => props.text,
(v) => {
StreamManager.updateStream(streamId, v ? String(v) : "", !!props.finish);
}
);
const stopFinishWatcher = watch(
() => props.finish,
(f) => {
StreamManager.updateStream(streamId, props.text ? String(props.text) : "", !!f);
if (f) {
stopForward();
stopFinishWatcher();
}
}
);
// 清理:组件卸载时停止转发(若仍存在)
onBeforeUnmount(() => {
try {
stopForward && stopForward();
stopFinishWatcher && stopFinishWatcher();
} catch (e) {}
});
// 传递 finished 参数,完成状态下不自动滚到底部
uni.navigateTo({ url: `/pages/ChatMain/ChatLongAnswer/index?streamId=${encodeURIComponent(streamId)}&finished=${props.finish ? '1' : '0'}` });
}
</script>
<style scoped lang="scss">
.icon-active {
margin-top: 1px;
color: $theme-color-500;
}
.answer-content {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
line-height: 16px;
max-height: 80px;
}
.border-left-4 {
border-left: 4px solid $theme-color-500;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<view class="tag-list">
<view
v-for="(item, index) in tags"
:key="index"
class="tag-item"
@click="handleClick(item)"
>
<text class="tag-text">{{ item }}</text>
</view>
</view>
</template>
<script setup>
import { ref, nextTick } from "vue";
import { onMounted } from "vue";
import {
SCROLL_TO_BOTTOM,
SEND_MESSAGE_CONTENT_TEXT,
} from "@/constant/constant";
const props = defineProps({
question: {
type: String,
default: "",
},
});
const tags = ref([]);
const handleClick = (item) => {
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item);
};
onMounted(() => {
tags.value = props.question.split(/[&|;]/).filter((tag) => tag.trim() !== "");
nextTick(() => {
setTimeout(() => {
uni.$emit(SCROLL_TO_BOTTOM, true);
}, 300);
});
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,23 @@
.tag-list {
display: flex;
flex-wrap: wrap;
padding: 6px 12px;
}
.tag-item {
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;
margin-bottom: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tag-text {
color: $theme-color-500;
font-size: $uni-font-size-base;
}

View File

@@ -0,0 +1,28 @@
<template>
<view class="w-full">
<template v-if="toolCall.picture && toolCall.picture.length > 0">
<ModuleTitle :title="图片详情" />
<ImageSwiper :images="toolCall.picture" thumbnailBottom="12px" />
</template>
<template v-if="toolCall.commodityList">
<DetailCardGoodsContentList :commodityList="toolCall.commodityList" />
</template>
</view>
</template>
<script setup>
import { defineProps } from "vue";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
import ImageSwiper from "@/components/ImageSwiper/index.vue";
import DetailCardGoodsContentList from "../DetailCardGoodsContentList/index.vue";
const props = defineProps({
toolCall: {
type: Object,
default: {},
},
});
</script>
<style scoped lang="scss"></style>

View File

@@ -0,0 +1,24 @@
<template>
<view class="container">
<ModuleTitle title="相关商品" />
<SwipeCards :cardsData="commodityList" />
</view>
</template>
<script setup>
import { defineProps } from "vue";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
import SwipeCards from "@/components/SwipeCards/index.vue";
const props = defineProps({
commodityList: {
type: Array,
default: [],
},
});
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,3 @@
.container {
margin: 6px 0 0;
}

View File

@@ -0,0 +1,37 @@
<template>
<view class="container">
<view v-for="item in themeDTOList" :key="item.title">
<RecommendPostsList :recommendTheme="item" />
</view>
</view>
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import { SCROLL_TO_BOTTOM } from "@/constant/constant";
import { discoveryCradComponent } from "@/request/api/MainPageDataApi";
import RecommendPostsList from "../RecommendPostsList/index.vue";
const themeDTOList = ref([]);
const loadDiscoveryCradComponent = async () => {
const res = await discoveryCradComponent();
if (res.code === 0 && res.data) {
themeDTOList.value = res.data.themeDTOList;
nextTick(() => {
setTimeout(() => {
uni.$emit(SCROLL_TO_BOTTOM, true);
}, 300);
});
}
};
onMounted(() => {
loadDiscoveryCradComponent();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,5 @@
.container {
width: 100%;
flex: 1;
margin-bottom: 12px;
}

View File

@@ -0,0 +1,39 @@
<template>
<view class="container">
<ModuleTitle :title="recommendTheme.themeName" />
<view class="container-scroll">
<view v-for="(item, index) in recommendTheme.recommendPostsList" :key="index">
<view class="mk-card-item" @click="sendReply(item)">
<image class="card-img" :src="item.coverPhoto" mode="widthFix"></image>
<text class="card-text">{{ item.topic }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { SEND_MESSAGE_CONTENT_TEXT, SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
import { defineProps } from "vue";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
const props = defineProps({
recommendTheme: {
type: Object,
default: {},
},
});
const sendReply = (item) => {
if (item.userInputContentType && item.userInputContentType === '1') {
const commonItem = { type: item.userInputContent, title: item.topic }
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, commonItem);
return;
}
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item.userInputContent);
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,33 @@
.container {
.container-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
margin-top: 4px;
.mk-card-item {
display: flex;
flex-direction: column;
align-items: start;
width: 188px;
height: 154px;
background-color: $uni-bg-color;
border-radius: 10px;
margin-right: 8px;
position: relative;
.card-img {
width: 188px;
height: 112px;
}
.card-text {
padding: 12px;
text-align: center;
font-weight: 500;
font-size: $uni-font-size-sm;
color: $uni-text-color;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,68 @@
<template>
<view class="container w-full border-box border-ff overflow-hidden rounded-24 flex flex-col">
<!-- 占位撑开 -->
<view class="w-vw"></view>
<view class="content flex flex-col m-4 border-box rounded-24">
<view class="flex flex-col p-6 border-box flex-items-center justify-center">
<image class="w-full rounded-20" :src="props.toolCall.componentNameParams.background" mode="widthFix" />
<!-- 左上角标签 -->
<view class="tag">
<view class="dot"></view>
<text class="tag-text">{{ props.toolCall.componentNameParams.superscript }}</text>
</view>
<image class="g_title mt-20" :src="props.toolCall.componentNameParams.title" />
<text class="font-size-14 font-400 color-white my-12 text-center">
{{ props.toolCall.componentNameParams.description }}
</text>
<view class="w-full border-box px-12 pb-8 flex flex-row" @click="jumpClick">
<view class="btn-bg w-full border-box p-4 rounded-24 flex flex-row">
<view class="btn-bg-sub w-full border-box p-16 rounded-20 flex flex-row flex-items-center flex-justify-center color-white text-center font-size-18 font-800">
{{ props.toolCall.componentNameParams.buttonName }}
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps, nextTick, onMounted, computed } from "vue";
import {
SCROLL_TO_BOTTOM,
} from "@/constant/constant";
import { getAccessToken } from "@/constant/token";
import { navigateTo } from "@/router";
const props = defineProps({
toolCall: {
type: Object,
default: {},
},
});
onMounted(() => {
nextTick(() => {
setTimeout(() => {
uni.$emit(SCROLL_TO_BOTTOM, true);
}, 300);
});
});
const jumpClick = () => {
const token = getAccessToken();
if (props.toolCall.componentNameParams.jumpUrl) {
navigateTo(props.toolCall.componentNameParams.jumpUrl, { token: token });
}
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,59 @@
.container {
background: rgba(255, 255, 255, 0.2);
box-shadow: 0px 9px 34px 0px rgba(27, 9, 91, 0.07);
}
.content {
background: linear-gradient(180deg, $theme-color-100 0%, $theme-color-500 100%);
border-radius: 24px 24px 24px 24px;
border: 1px solid #FFFFFF;
position: relative;
}
.g_title {
width: 197px;
height: 42px;
}
.btn-bg {
background: rgba(255, 255, 255, 0.32);
border: 0.66px solid rgba(255, 255, 255, 0.32);
}
.btn-bg-sub {
background: $theme-color-500;
box-shadow: inset 0px 0px 41px 0px $theme-color-50, inset 0px 0px 15px 0px $theme-color-100;
}
/* 左上角标签容器 */
.tag {
position: absolute;
top: 20px;
left: 20px;
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: 999rpx;
backdrop-filter: blur(6rpx);
background: rgba(255,255,255,0.79);
box-shadow: inset 0px 1px 2px 0px rgba(255,255,255,0.46);
}
/* 小绿点 */
.dot {
width: 15px;
height: 15px;
border-radius: 50%;
background: $theme-color-500;
margin-right: 8px;
}
/* 文字 */
.tag-text {
font-size: 12px;
color: $theme-color-500;
font-weight: 600;
}

View File

@@ -0,0 +1,112 @@
<template>
<uni-popup ref="popup" type="bottom" :safe-area="false">
<view class="popup-content border-box pt-12 pl-12 pr-12">
<view class="header flex flex-items-center pb-12">
<view class="title flex-full text-center font-size-17 color-000 font-500 ml-24">更多服务</view>
<uni-icons type="close" size="24" color="#CACFD8" @click="close" />
</view>
<view class="list bg-white border-box pl-20 pr-20">
<view class="item border-box border-bottom pt-20 pb-20" v-for="(item, index) in list" :key="index">
<view class="flex flex-items-center flex-justify-center">
<image v-if="item.icon" class="left" :src="item.icon" />
<view class="center flex-full">
<view class="font-size-16 color-000 line-height-24 font-500">
{{ item.title }}
</view>
<view class="font-size-12 color-A3A3A3 line-height-16">
{{ item.content }}
</view>
</view>
<view class="right border-box font-size-12 color-white line-height-16" @click="handleClick(item)">
{{ item.btnText }}
</view>
</view>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref } from "vue";
import { Command } from "@/model/ChatModel";
import { SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
import { checkToken } from "@/hooks/useGoLogin";
const popup = ref(null);
const list = ref([
{
icon: "https://oss.nianxx.cn/mp/static/version_101/home/ksyd.png",
title: "快速预定",
content: "预定门票、房间、餐食",
btnText: "去预定",
type: Command.quickBooking,
path: "/pages-quick/list",
},
{
icon: "https://oss.nianxx.cn/mp/static/version_101/home/tsfx.png",
title: "探索发现",
content: "发现景点、活动、特色内容",
btnText: "去探索",
type: Command.discovery,
path: "",
},
{
icon: "https://oss.nianxx.cn/mp/static/version_101/home/mddd.png",
title: "我的订单",
content: "查看门票、住宿、餐饮等订单",
btnText: "去查看",
type: Command.myOrder,
path: "/pages-order/order/list",
},
{
icon: "https://oss.nianxx.cn/mp/static/version_101/home/wdgd.png",
title: "呼叫服务",
content: "查看呼叫服务、进度与处理情况",
btnText: "去查看",
type: Command.myWorkOrder,
path: "/pages-service/order/list",
},
{
icon: "https://oss.nianxx.cn/mp/static/version_101/home/fkyj.png",
title: "反馈意见",
content: "提交使用问题、建议与需求",
btnText: "去反馈",
type: Command.feedbackCard,
path: "",
},
]);
const open = () => {
popup.value && popup.value.open();
};
const close = () => {
popup.value && popup.value.close();
};
const handleClick = (item) => {
close();
if (item.path) {
checkToken().then(() => {
uni.navigateTo({ url: item.path });
});
return;
}
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, item);
};
// 接收更多服务
uni.$on("SHOW_MORE_POPUP", () => {
open();
});
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,29 @@
.popup-content {
background-color: #f5f7fa;
border-radius: 15px 15px 0 0;
padding-bottom: Max(env(safe-area-inset-bottom), 12px) !important;
}
.list {
border-radius: 15px;
}
.item {
gap: 20px;
&:last-child {
border-bottom: none;
}
}
.left {
width: 24px;
height: 24px;
margin-right: 12px;
}
.right {
background-color: $theme-color-500;
border-radius: 5px;
padding: 6px;
}

View File

@@ -0,0 +1,51 @@
<template>
<view class="w-full bg-white border-box border-ff overflow-hidden rounded-20 flex flex-col">
<!-- 占位撑开 -->
<view class="w-vw"></view>
<view class="flex flex-items-center flex-justify-between p-12 border-box" @click="openMap">
<view class="rounded-16 p-16 bg-theme-color-50">
<view class="w-32 h-32 rounded-full bg-white flex flex-items-center flex-justify-center">
<uni-icons type="location" size="16" color="#171717" />
</view>
</view>
<view class="center ml-12">
<view class="font-color-900 font-size-14">打开导览地图</view>
<view class="font-color-500 font-size-12 mt-6">距你 {{ distance }} · 步行 {{ walk }}</view>
</view>
<view class="ml-12 flex flex-items-center flex-justify-center w-32 h-32 rounded-full bg-F2F5F8">
<uni-icons class="mb-2" type="right" size="12" color="#99A0AE" />
</view>
</view>
</view>
</template>
<script>
export default {
name: 'OpenMapComponent',
props: {
distance: {
type: String,
default: '500m'
},
walk: {
type: String,
default: '8分钟'
}
},
methods: {
openMap() {
this.$emit('open')
}
}
}
</script>
<style scoped>
.center {
flex: 1 1 auto;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<view class="date-picker">
<view class="date-list">
<view
v-for="(item, index) in dates"
:key="index"
class="date-item"
:class="{ active: index === activeIndex }"
@click="selectDate(index)"
>
<text class="label">{{ item.label }}</text>
<text class="date">{{ item.date }}</text>
</view>
<!-- 日历按钮 -->
<view class="calendar-btn btn-bom" @click="openCalendar">
<image
src="https://oss.nianxx.cn/mp/static/booking_calendar.png"
mode="widthFix"
class="calendar-img"
/>
<text class="calendar-text">日历</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
const emit = defineEmits(["update:date"]); // 声明事件
const activeIndex = ref(2); // 默认今天
const dates = ref([]);
// 初始化日期(前天、昨天、今天、明天、后天)
const initDates = () => {
const today = new Date();
const labels = ["前天", "昨天", "今天", "明天", "后天"];
for (let i = -2; i <= 2; i++) {
const d = new Date(today);
d.setDate(today.getDate() + i);
const month = d.getMonth() + 1;
const day = d.getDate();
dates.value.push({
label: labels[i + 2],
date: `${month}/${day}`,
fullDate: `${d.getFullYear()}-${String(month).padStart(2, "0")}-${String(
day
).padStart(2, "0")}`,
});
}
};
const selectDate = (index) => {
activeIndex.value = index;
emit("update:date", dates.value[index]); // 传回父组件
};
const openCalendar = () => {
uni.$emit("openCalendar");
uni.$on("selectCalendarDate", (date) => {
emit("update:date", { fullDate: date });
uni.$off("selectCalendarDate");
});
};
onMounted(() => {
initDates();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,57 @@
.date-picker {
background: rgba(140, 236, 255, 0.24);
padding: 8rpx 0;
border-radius: 16rpx;
margin: 12px 0 6px;
min-width: 325px;
box-shadow: 0 4rpx 8rpx rgba(0, 0, 0, 0.1); /* 阴影 */
}
.date-list {
display: flex;
}
.date-item,
.calendar-btn {
flex: 1; /* 等宽 */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 12rpx 0;
border-radius: 16rpx;
}
.label {
font-size: 24rpx;
color: $uni-text-color-grey;
}
.date {
font-size: 28rpx;
font-weight: bold;
color: #555;
}
.date-item.active {
background-color: $theme-color-500;
}
.date-item.active .label,
.date-item.active .date {
color: #fff;
}
/* 日历按钮 */
.calendar-btn {
background: #fff;
border-radius: 0 16rpx 16rpx 0;
margin: -8rpx 0;
}
.calendar-img {
width: 16px;
height: 16px;
}
.calendar-text {
font-size: 22rpx;
color: #666;
margin-top: 4rpx;
}

View File

@@ -0,0 +1,66 @@
<template>
<view class="container">
<QuickBookingCalender class="calendar" @update:date="onDateSelected" />
<view
v-for="item in commodityGroupDTOList"
:key="commodityGroupKey(item.title)"
>
<QuickBookingContentList :commodityDTO="item" />
</view>
</view>
</template>
<script setup>
import QuickBookingCalender from "../QuickBookingCalender/index.vue";
import QuickBookingContentList from "../QuickBookingContentList/index.vue";
import { ref, nextTick } from "vue";
import { onMounted } from "vue";
import { quickBookingComponent } from "@/request/api/MainPageDataApi";
import { SCROLL_TO_BOTTOM } from "@/constant/constant";
const selectedDate = ref({});
const commodityGroupDTOList = ref([]);
const formattedDate = ref("");
const loadQuickBookingComponent = async () => {
formattedDate.value = formatDate(selectedDate.value.fullDate || new Date());
const res = await quickBookingComponent(formattedDate.value);
if (res.code === 0 && res.data) {
commodityGroupDTOList.value = res.data.commodityGroupDTOList;
nextTick(() => {
setTimeout(() => {
uni.$emit(SCROLL_TO_BOTTOM, true);
}, 300);
});
}
};
const onDateSelected = (date) => {
console.log("Selected date:", date);
selectedDate.value = date;
loadQuickBookingComponent();
};
// 格式化日期为 yyyy-MM-dd
const formatDate = (date) => {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
};
const commodityGroupKey = (title) => {
return `${title}${formattedDate.value}`;
};
onMounted(() => {
console.log("=============");
loadQuickBookingComponent();
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,9 @@
.container {
width: 100%;
flex: 1;
margin-bottom: 12px;
.calendar {
width: 100%;
}
}

View File

@@ -0,0 +1,71 @@
<template>
<view class="container">
<ModuleTitle :title="commodityDTO.title" />
<view class="container-scroll">
<view
v-for="(item, index) in commodityDTO.commodityList"
:key="`${item.commodityId}-${index}`"
>
<view class="mk-card-item" @click="placeOrderHandle(item)">
<!-- <view class="card-badge">超值推荐</view> -->
<image class="card-img" :src="item.commodityIcon" mode="aspectFill" />
<view class="card-content">
<view class="card-title-column">
<text class="card-title">{{ item.commodityName }}</text>
<view
class="card-tags"
v-for="tag in item.commodityTradeRuleList"
:key="tag"
>
<text class="card-tag">{{ tag }}</text>
</view>
</view>
<template
v-for="(serviceItem, index) in item.commodityServices"
:key="serviceItem.serviceTitle"
>
<view v-if="index < 3" class="card-desc"
>· {{ serviceItem.serviceTitle }}</view
>
</template>
<view class="card-bottom-row">
<view class="card-price-row">
<text class="card-price-fu"></text>
<text class="card-price">{{ item.commodityPrice }}</text>
<text class="card-unit">/{{ item.stockUnitLabel }}</text>
</view>
<text class="card-btn">下单</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps } from "vue";
import { checkToken } from "@/hooks/useGoLogin";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
const props = defineProps({
commodityDTO: {
type: Object,
default: {},
},
});
/// 去下单
const placeOrderHandle = (item) => {
checkToken().then(() => {
uni.navigateTo({
url: `/pages/goods/index?commodityId=${item.commodityId}`,
});
});
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,137 @@
.container {
.container-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
overflow-y: hidden;
margin: 4px 0;
.mk-card-item {
position: relative;
display: flex;
flex-direction: column;
align-items: start;
width: 188px;
// height: 244px;
background-color: $uni-bg-color;
border-radius: 10px;
margin-right: 8px;
padding-bottom: 12px;
.card-badge {
position: absolute;
top: 8px;
left: 8px;
background: #ffe7b2;
color: #b97a00;
font-size: $uni-font-size-sm;
padding: 2px 8px;
border-radius: 4px;
z-index: 2;
}
.card-img {
width: 188px;
height: 114px;
border-radius: 10px;
object-fit: cover; /* 确保图片不变形,保持比例裁剪 */
flex-shrink: 0; /* 防止图片被压缩 */
}
.card-content {
box-sizing: border-box;
padding: 10px 12px 0 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: start;
width: 100%;
flex: 1; /* 让内容区域占据剩余空间 */
overflow: hidden; /* 防止内容溢出 */
}
.card-title-column {
display: flex;
align-items: start;
flex-direction: column;
width: 100%;
}
.card-title {
font-size: $uni-font-size-lg;
font-weight: bold;
color: #222;
width: 100%;
/* 限制标题最多显示两行 */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4;
max-height: 2.8em; /* 2行的高度 */
}
.card-tags {
display: flex;
flex-direction: row;
align-items: start;
padding: 6px 0;
}
.card-tag {
color: #ff6600;
font-size: 10px;
border-radius: 4px;
padding: 0 6px;
margin-left: 2px;
border: 1px solid #ff6600;
}
.card-desc {
font-size: 13px;
color: #888;
margin-top: 2px;
}
.card-bottom-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
width: 100%;
}
.card-price-row {
.card-price-fu {
color: #ff6600;
font-size: 11px;
font-weight: normal;
}
.card-price {
color: #ff6600;
font-size: $uni-font-size-lg;
font-weight: bold;
}
.card-unit {
font-size: 11px;
color: #888;
font-weight: normal;
margin-left: 2px;
}
}
.card-btn {
background: #ff6600;
color: #fff;
font-size: 15px;
border-radius: 20px;
padding: 0 18px;
height: 32px;
line-height: 32px;
}
}
}
}

View File

@@ -0,0 +1,26 @@
<template>
<view class="container">
<view v-for="item in recommendThemeList" :key="item.themeName">
<RecommendPostsList
v-if="item.recommendPostsList.length > 0"
:recommendTheme="item"
/>
</view>
</view>
</template>
<script setup>
import RecommendPostsList from "../RecommendPostsList/index.vue";
import { defineProps } from "vue";
const props = defineProps({
recommendThemeList: {
type: Array,
default: [],
},
});
</script>
<style scoped lang="scss">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,5 @@
.container {
width: 100%;
flex: 1;
margin-bottom: 12px;
}

View File

@@ -0,0 +1,48 @@
<template>
<view class="container">
<ModuleTitle :title="recommendTheme.themeName" />
<view class="container-scroll font-size-0 scroll-x whitespace-nowrap">
<view class="card-item bg-white inline-block rounded-20 mr-8"
v-for="(item, index) in recommendTheme.recommendPostsList" :key="index" @click="sendReply(item)">
<view class="m-4 relative">
<image class="card-img rounded-16 relative z-10" :src="item.coverPhoto" mode="aspectFill" />
<view class="shadow absolute rounded-16"></view>
</view>
<view class="card-text border-box">
<view class="font-size-11 color-99A0AE ellipsis-1">
{{ item.topic }}
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps } from "vue";
import { SEND_MESSAGE_CONTENT_TEXT, SEND_MESSAGE_COMMAND_TYPE } from "@/constant/constant";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
const props = defineProps({
recommendTheme: {
type: Object,
default: {},
},
});
const sendReply = (item) => {
if (item.userInputContentType && item.userInputContentType === '1') {
const commonItem = { type: item.userInputContent, title: item.topic }
uni.$emit(SEND_MESSAGE_COMMAND_TYPE, commonItem);
return;
}
uni.$emit(SEND_MESSAGE_CONTENT_TEXT, item.userInputContent);
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,26 @@
.container-scroll {
margin: 4px 0 6px;
}
.card-item {
width: 128px;
}
.card-img {
height: 120px;
width: 120px;
}
.shadow {
background-color: #e5e8ee;
height: 96px;
width: 96px;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
z-index: 1;
}
.card-text {
padding: 4px 8px 8px;
}