feat: 调整首页组件目录结构

This commit is contained in:
duanshuwen
2025-09-21 14:26:04 +08:00
parent 55fd7c7ee6
commit d33ad9a9ce
52 changed files with 336 additions and 349 deletions

View File

@@ -0,0 +1,45 @@
<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(item)">
<image :src="item.activityCover" mode="aspectFill"></image>
<view class="corner-btn">快速预定</view>
</view>
</swiper-item>
</swiper>
</view>
</template>
<script setup>
import { ref } from "vue";
import { SEND_COMMAND_TEXT } from "@/constant/constant";
const autoplay = ref(true);
const interval = ref(3000);
const duration = ref(500);
const props = defineProps({
activityList: {
type: Array,
default: [],
},
});
const handleClick = (item) => {
uni.$emit(SEND_COMMAND_TEXT, "快速预定");
};
</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;
image {
width: 100%;
height: 100%;
border-radius: 8px;
display: block;
}
.corner-btn {
position: absolute;
right: 12px;
bottom: 12px;
background-color: #ffeb00;
color: #333;
font-size: 14px;
font-weight: 500;
padding: 4px 12px;
border-radius: 20px;
line-height: 1.5;
}
}
}

View File

@@ -0,0 +1,45 @@
<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, defineEmits } from "vue";
import { onMounted } from "vue";
import { SCROLL_TO_BOTTOM } from "@/constant/constant";
const props = defineProps({
question: {
type: String,
default: "",
},
});
const tags = ref([]);
const emits = defineEmits(["replySent"]);
const handleClick = (item) => {
emits("replySent", 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,21 @@
.tag-list {
display: flex;
flex-wrap: wrap;
padding: 6px 12px;
}
.tag-item {
background-color: #ffffff;
border-radius: 8px;
padding: 4px 10px;
margin-right: 8px;
margin-bottom: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tag-text {
color: #00a6ff; /* 蓝色文字,可根据设计调整 */
font-size: 14px;
}

View File

@@ -0,0 +1,30 @@
<template>
<view class="container">
<template v-if="toolCall.picture && toolCall.picture.length > 0">
<ModuleTitle :title="图片详情" />
<ImageSwiper :images="toolCall.picture" />
</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">
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,4 @@
.container {
width: 100%;
padding: 12px 0;
}

View File

@@ -0,0 +1,73 @@
<template>
<view class="container">
<ModuleTitle title="相关商品" />
<view class="container-scroll">
<view
v-for="(item, index) in commodityList"
:key="`${item.commodityId}-${index}`"
>
<view class="mk-card-item" @click="placeOrderHandle(item)">
<image
class="card-img"
:src="item.commodityPhoto"
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.specificationPrice }}</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({
commodityList: {
type: Array,
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,138 @@
.container {
margin: 6px 0 0;
.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;
background-color: #ffffff;
border-radius: 10px;
margin-right: 8px;
padding-bottom: 12px;
.card-badge {
position: absolute;
top: 8px;
left: 8px;
background: #ffe7b2;
color: #b97a00;
font-size: 12px;
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: 16px;
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: 16px;
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,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,42 @@
<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 { RECOMMEND_POSTS_TITLE } from "@/constant/constant";
import { defineProps } from "vue";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
const props = defineProps({
recommendTheme: {
type: Object,
default: {},
},
});
const sendReply = (item) => {
const topic = item.userInputContent || item.topic.replace(/^#/, "");
uni.$emit(RECOMMEND_POSTS_TITLE, topic);
};
</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: #ffffff;
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: 12px;
color: #333333;
}
}
}
}

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="/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: #999;
}
.date {
font-size: 28rpx;
font-weight: bold;
color: #555;
}
.date-item.active {
background-color: #00a6ff;
}
.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: #ffffff;
border-radius: 10px;
margin-right: 8px;
padding-bottom: 12px;
.card-badge {
position: absolute;
top: 8px;
left: 8px;
background: #ffe7b2;
color: #b97a00;
font-size: 12px;
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: 16px;
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: 16px;
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,41 @@
<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="aspectFill" />
<view class="overlay-gradient">
<text class="overlay-text">{{ item.topic }}</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { defineProps } from "vue";
import { RECOMMEND_POSTS_TITLE } from "@/constant/constant";
import ModuleTitle from "@/components/ModuleTitle/index.vue";
const props = defineProps({
recommendTheme: {
type: Object,
default: {},
},
});
const sendReply = (item) => {
const topic = item.userInputContent || item.topic.replace(/^#/, "");
uni.$emit(RECOMMEND_POSTS_TITLE, topic);
};
</script>
<style lang="scss" scoped>
@import "./styles/index.scss";
</style>

View File

@@ -0,0 +1,53 @@
.container {
width: 100%;
}
.container-scroll {
display: flex;
flex-direction: row;
overflow-x: auto;
margin: 4px 0 6px;
}
.mk-card-item {
flex-shrink: 0; /* 关键:防止 flex 布局压缩子元素宽度 */
width: 142px;
height: 126px;
border-radius: 10px;
overflow: hidden;
margin-right: 8px;
position: relative;
.card-img {
width: 100%;
height: 100%;
display: block;
}
/* 渐变背景层 */
.overlay-gradient {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50px; /* 渐变层高度,可调 */
background: linear-gradient(
to bottom,
rgba(0, 0, 0, 0.001) 0%,
rgba(0, 0, 0, 0.5) 100%
);
display: flex;
align-items: flex-end; /* 文字贴近底部 */
padding: 0 8px 6px; /* 内边距让文字与边缘保持距离 */
box-sizing: border-box;
}
/* 渐变层上的文字 */
.overlay-text {
color: #fff;
font-weight: 500;
font-size: 12px;
text-align: left;
width: 100%;
}
}