feat(order): add order detail and list pages with components for order management
- Implemented order detail page with components for displaying order status, user info, and refund options. - Created order list page with pagination and order cards for displaying all orders. - Added styles for order detail and list pages. - Developed a prompt document outlining component requirements for the order management system. - Introduced a new Card component for quick booking with a responsive design. - Enhanced Tabs component for better navigation between different categories. - Integrated z-paging for efficient data loading and management in order and quick booking lists. - Added service order card component for displaying service requests with call functionality. - Updated main CSS for improved viewport handling.
This commit is contained in:
81
src/pages/quick/components/Card/index.vue
Normal file
81
src/pages/quick/components/Card/index.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div
|
||||
class="card bg-white border-box p-8 rounded-12 flex flex-items-start m-12"
|
||||
@click.stop="handleClick(item)"
|
||||
>
|
||||
<img class="left rounded-10" :src="item.commodityPhoto" mode="aspectFill" />
|
||||
<div class="right border-box flex-full pl-12">
|
||||
<div class="font-size-16 line-height-24 color-171717 mb-4">
|
||||
{{ item.commodityName }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.commodityFacility"
|
||||
class="font-size-12 line-height-16 color-99A0AE mb-4 ellipsis-1"
|
||||
>
|
||||
{{ item.commodityFacility.join(" ") }}
|
||||
</div>
|
||||
<div class="font-size-12 line-height-18 color-43669A">
|
||||
{{ item.commodityTradeRuleList.join(" / ") }}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-items-center flex-justify-end">
|
||||
<span
|
||||
class="amt font-size-18 font-500 font-family-misans-vf line-height-24 color-FF3D60 mr-4"
|
||||
>
|
||||
{{ item.specificationPrice }}
|
||||
</span>
|
||||
<span
|
||||
v-if="item.stockUnitLabel"
|
||||
class="font-size-12 line-height-16 color-99A0AE"
|
||||
>
|
||||
/{{ item.stockUnitLabel }}
|
||||
</span>
|
||||
<span class="btn border-box rounded-10 color-white ml-16">订</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from "vue";
|
||||
import { useSelectedDateStore } from "@/store";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({
|
||||
commodityPhoto: "",
|
||||
commodityId: "",
|
||||
commodityName: "",
|
||||
specificationPrice: "",
|
||||
commodityServices: [],
|
||||
commodityTags: [], // 商品标签
|
||||
commodityTradeRuleList: [], // 交易规则列表
|
||||
specificationId: "", // 规格ID
|
||||
stockUnitLabel: "", // 库存单位
|
||||
}),
|
||||
},
|
||||
selectedDate: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
const selectedDateStore = useSelectedDateStore();
|
||||
|
||||
const navigateToPage = (commodityId, path) => {
|
||||
const { startDate, endDate, totalDays } = props.selectedDate;
|
||||
selectedDateStore.setData({ startDate, endDate, totalDays });
|
||||
|
||||
uni.navigateTo({ url: `${path}?commodityId=${commodityId}` });
|
||||
};
|
||||
|
||||
const handleClick = ({ commodityId }) =>
|
||||
navigateToPage(commodityId, "/pages/goods/index");
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
22
src/pages/quick/components/Card/styles/index.scss
Normal file
22
src/pages/quick/components/Card/styles/index.scss
Normal file
@@ -0,0 +1,22 @@
|
||||
.left {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.right {
|
||||
max-width: 259px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.amt {
|
||||
&::before {
|
||||
content: "¥";
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(90deg, #ff3d60 57%, #ff990c 100%);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
146
src/pages/quick/components/Tabs/index.vue
Normal file
146
src/pages/quick/components/Tabs/index.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="tab-container relative">
|
||||
<div class="tab-wrapper flex flex-items-center">
|
||||
<div
|
||||
v-for="(item, index) in tabList"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'tab-item flex flex-items-center flex-justify-center relative',
|
||||
activeIndex === index && 'tab-item-active',
|
||||
]"
|
||||
@click="handleTabClick(index)"
|
||||
>
|
||||
<div class="tab-item-inner flex flex-items-center">
|
||||
<uni-icons
|
||||
:class="['icon mr-4', activeIndex === index && 'icon-active']"
|
||||
fontFamily="znicons"
|
||||
size="20"
|
||||
color="opacity"
|
||||
>
|
||||
{{ zniconsMap[item.iconCode] }}
|
||||
</uni-icons>
|
||||
|
||||
<span
|
||||
:class="[
|
||||
'font-size-16 font-500 color-525866 ',
|
||||
activeIndex === index && 'tab-span-active',
|
||||
]"
|
||||
>
|
||||
{{ item.iconTitle }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 每项内的下划线指示器,通过类控制显示/隐藏 -->
|
||||
<div
|
||||
class="tab-item-indicator"
|
||||
:class="{ visible: activeIndex === index }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { zniconsMap } from "@/static/fonts/znicons";
|
||||
import { commodityTypePageList } from "@/request/api/GoodsApi";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
tabs: {
|
||||
type: Array,
|
||||
default: () => [
|
||||
{ iconTitle: "客房", typeCode: "0", iconCode: "zn-nav-room" },
|
||||
{ iconTitle: "门票", typeCode: "1", iconCode: "zn-nav-ticket" },
|
||||
{ iconTitle: "餐食", typeCode: "2", iconCode: "zn-nav-meal" },
|
||||
{ iconTitle: "套餐", typeCode: "3", iconCode: "zn-package" },
|
||||
{ iconTitle: "文创", typeCode: "4", iconCode: "zn-package" },
|
||||
],
|
||||
},
|
||||
defaultActive: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
indicatorColor: {
|
||||
type: String,
|
||||
default: "#1890ff",
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["change", "update:modelValue"]);
|
||||
|
||||
// 响应式数据
|
||||
const activeIndex = ref(props.defaultActive);
|
||||
const tabList = ref([]);
|
||||
|
||||
// 处理Tab点击
|
||||
const handleTabClick = (index) => {
|
||||
changeTabItem(index);
|
||||
};
|
||||
|
||||
// 支持 force 参数,强制触发(即使 index 与当前相同)
|
||||
const changeTabItem = (index, force = false) => {
|
||||
if (!force && activeIndex.value === index) return;
|
||||
activeIndex.value = index;
|
||||
|
||||
emit("change", {
|
||||
index,
|
||||
item: tabList.value[index],
|
||||
});
|
||||
emit("update:modelValue", index);
|
||||
};
|
||||
|
||||
// 监听tabs变化
|
||||
watch(
|
||||
() => props.tabs,
|
||||
(newTabs) => {
|
||||
tabList.value = newTabs;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 监听defaultActive变化
|
||||
watch(
|
||||
() => props.defaultActive,
|
||||
(newActive) => {
|
||||
if (newActive !== activeIndex.value) {
|
||||
activeIndex.value = newActive;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
setActiveIndex: (index) => {
|
||||
if (index >= 0 && index < tabList.value.length) {
|
||||
handleTabClick(index);
|
||||
}
|
||||
},
|
||||
getActiveIndex: () => activeIndex.value,
|
||||
getActiveItem: () => tabList.value[activeIndex.value],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getCommodityTypePageList();
|
||||
});
|
||||
|
||||
// 获取商品类型列表(示例方法,实际使用时根据需要调用)
|
||||
const getCommodityTypePageList = async () => {
|
||||
const res = await commodityTypePageList({ size: 20, current: 1 });
|
||||
if (res && res.data && res.data.records) {
|
||||
tabList.value = res.data.records;
|
||||
} else {
|
||||
tabList.value = props.tabs;
|
||||
}
|
||||
// 加载完成后强制选中第一个项并通知外部
|
||||
if (tabList.value && tabList.value.length > 0) {
|
||||
changeTabItem(0, true);
|
||||
} else {
|
||||
changeTabItem(props.defaultActive, true);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./styles/index.scss";
|
||||
</style>
|
||||
90
src/pages/quick/components/Tabs/styles/index.scss
Normal file
90
src/pages/quick/components/Tabs/styles/index.scss
Normal file
@@ -0,0 +1,90 @@
|
||||
.tab-wrapper {
|
||||
background-color: $theme-color-100;
|
||||
height: 48px;
|
||||
overflow-x: auto;
|
||||
/* 支持横向滚动 */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* 平滑滚动(移动端) */
|
||||
white-space: nowrap;
|
||||
/* 防止换行 */
|
||||
justify-content: flex-start;
|
||||
/* 覆盖工具类,靠左排列以便滚动 */
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
height: 100%;
|
||||
flex: 0 0 auto;
|
||||
/* 不让子项拉伸,按内容宽度排列 */
|
||||
padding: 0 12px;
|
||||
/* 增加横向间距,便于触控 */
|
||||
min-width: 68px;
|
||||
/* 保证可点击区域 */
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
color: #525866;
|
||||
}
|
||||
|
||||
.icon-active {
|
||||
color: $theme-color-500;
|
||||
}
|
||||
|
||||
/* 组件模板中使用了绝对定位的内部元素,为保证父元素宽度基于内容,重置该子元素为静态布局 */
|
||||
.tab-item>.absolute {
|
||||
position: static !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-item-active {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 0;
|
||||
right: 4px;
|
||||
bottom: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 20px 20px 0 0;
|
||||
transform: perspective(40px) rotateX(6deg) translate(0, -1px);
|
||||
transform-origin: bottom bottom;
|
||||
box-shadow: 0 -0.5px 0 $theme-color-500;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-text-active {
|
||||
color: $theme-color-500;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/* 已改为每项内部指示器,移除了全局指示器样式 */
|
||||
|
||||
/* 每项内的指示器(替代全局指示器) */
|
||||
.tab-item-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
/* 确保内容(icon/text)位于 .tab-item-active::before 之上 */
|
||||
}
|
||||
|
||||
.tab-item-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scaleX(0.9);
|
||||
height: 3px;
|
||||
width: 24px;
|
||||
background-color: $theme-color-500;
|
||||
border-radius: 3px 3px 0 0;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.tab-item-indicator.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) scaleX(1);
|
||||
}
|
||||
151
src/pages/quick/list.vue
Normal file
151
src/pages/quick/list.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<z-paging
|
||||
ref="paging"
|
||||
v-model="dataList"
|
||||
use-virtual-list
|
||||
:force-close-inner-list="true"
|
||||
cell-height-mode="dynamic"
|
||||
safe-area-inset-bottom
|
||||
@query="queryList"
|
||||
>
|
||||
<template #top>
|
||||
<TopNavBar title="快速预定" :background="$theme - color - 100" />
|
||||
|
||||
<Tabs @change="handleTabChange" />
|
||||
|
||||
<!-- 选择入住、离店日期 0:是酒店 -->
|
||||
<div
|
||||
v-if="didSelectedTabItem && didSelectedTabItem.orderType == 0"
|
||||
class="bg-white border-box flex flex-items-center p-12"
|
||||
>
|
||||
<div class="in flex flex-items-center">
|
||||
<span class="font-size-11 font-500 color-99A0AE mr-4">入住</span>
|
||||
<span class="font-size-14 font-500 color-171717">
|
||||
{{ selectedDate.startDate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 几晚 -->
|
||||
<span
|
||||
class="nights bg-E5E8EE border-box font-size-11 font-500 color-525866 rounded-50 ml-8 mr-8"
|
||||
>
|
||||
{{ selectedDate.totalDays }}晚
|
||||
</span>
|
||||
|
||||
<div class="out flex flex-items-center">
|
||||
<span class="font-size-11 font-500 color-99A0AE mr-4">离店</span>
|
||||
<span class="font-size-14 font-500 color-171717">
|
||||
{{ selectedDate.endDate }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 日期图标 -->
|
||||
<uni-icons
|
||||
class="ml-auto"
|
||||
type="calendar"
|
||||
size="24"
|
||||
color="$theme-color-500"
|
||||
@click="calendarVisible = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Card
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
:item="item"
|
||||
:selectedDate="selectedDate"
|
||||
/>
|
||||
</z-paging>
|
||||
|
||||
<!-- 日历组件 -->
|
||||
<Calender
|
||||
:visible="calendarVisible"
|
||||
mode="range"
|
||||
:default-value="selectedDate"
|
||||
@close="handleCalendarClose"
|
||||
@range-select="handleDateSelect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import TopNavBar from "@/components/TopNavBar/index.vue";
|
||||
import Calender from "@/components/Calender/index.vue";
|
||||
import Tabs from "./components/Tabs/index.vue";
|
||||
import Card from "./components/Card/index.vue";
|
||||
import { quickBookingList } from "@/request/api/GoodsApi";
|
||||
import { DateUtils } from "@/utils";
|
||||
|
||||
const calendarVisible = ref(false);
|
||||
const selectedDate = ref({
|
||||
startDate: DateUtils.formatDate(), // 当天日期
|
||||
endDate: DateUtils.formatDate(new Date(Date.now() + 24 * 60 * 60 * 1000)), // 第二天日期
|
||||
totalDays: 1,
|
||||
});
|
||||
const dataList = ref([]);
|
||||
const paging = ref(null);
|
||||
// 当前选中项
|
||||
const didSelectedTabItem = ref(null);
|
||||
|
||||
const queryList = async (pageNum = 1, pageSize = 10) => {
|
||||
if (!didSelectedTabItem.value) {
|
||||
paging.value.complete([]);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const params = {
|
||||
commodityTypeCode: didSelectedTabItem.value.typeCode,
|
||||
size: pageSize,
|
||||
current: pageNum,
|
||||
};
|
||||
|
||||
if (didSelectedTabItem.value.orderType == 0) {
|
||||
params.checkInDate = selectedDate.value.startDate;
|
||||
params.checkOutDate = selectedDate.value.endDate;
|
||||
}
|
||||
|
||||
const res = await quickBookingList(params);
|
||||
console.log("API响应:", res.data.records);
|
||||
|
||||
if (res && res.data && res.data.records) {
|
||||
const records = res.data.records;
|
||||
|
||||
// 完成数据加载,第二个参数表示是否还有更多数据
|
||||
paging.value.complete(records);
|
||||
} else {
|
||||
// 没有数据
|
||||
paging.value.complete([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("查询列表失败:", error);
|
||||
// 加载失败
|
||||
paging.value.complete(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = ({ item }) => {
|
||||
console.log("选中的tab item: ", item);
|
||||
didSelectedTabItem.value = item;
|
||||
paging.value.reload();
|
||||
};
|
||||
|
||||
// 处理日历关闭
|
||||
const handleCalendarClose = () => {
|
||||
calendarVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理日期选择
|
||||
const handleDateSelect = (data) => {
|
||||
selectedDate.value = data;
|
||||
calendarVisible.value = false;
|
||||
console.log("选择的日期:", data);
|
||||
paging.value.reload();
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.nights {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user