This commit is contained in:
2026-05-11 00:23:48 +08:00
parent 5f579593a3
commit a61ffb6df2
7 changed files with 3044 additions and 1107 deletions
+359 -303
View File
@@ -1,29 +1,26 @@
<template>
<view class="page-index">
<!-- 搜索面板 - 使用毛玻璃效果 -->
<!-- 搜索面板 -->
<view class="search-panel">
<base-card class="search-card" variant="elevated" :shadow="false">
<!-- 位置 -->
<!-- 城市选择 -->
<form-field
type="select"
:display-value="selectedCity"
placeholder="选择城市"
:display-value="searchParams.city"
placeholder="选择目的地城市"
right-icon="map"
:show-select-arrow="false"
@click="openCityPicker"
></form-field>
/>
<!-- 日期 -->
<form-field
type="custom"
@click="openDatePicker"
>
<!-- 入住离店日期 -->
<form-field type="custom" @click="openDatePicker">
<view class="date-picker-content">
<view class="date-section">
<text class="date-label">{{ checkInDesc }}入住</text>
<text class="date-value">{{ checkInLabel }}</text>
</view>
<view class="nights-tag">{{ nightCount }}</view>
<view class="nights-badge">{{ nightCount }}</view>
<view class="date-section">
<text class="date-label">{{ checkOutDesc }}离店</text>
<text class="date-value">{{ checkOutLabel }}</text>
@@ -31,112 +28,128 @@
</view>
</form-field>
<!-- 关键词 -->
<!-- 搜索关键词 -->
<form-field
type="select"
:display-value="searchKeyword"
placeholder="输入关键词搜索酒店"
:display-value="searchParams.keyword"
placeholder="搜索酒店、民宿、位置"
right-icon="search"
@click="goLocationSearch"
></form-field>
/>
<!-- 价格筛选 -->
<!-- 价格区间 -->
<form-field
type="select"
:display-value="priceLabel !== '价格' ? priceLabel : ''"
placeholder="价格"
:display-value="priceRangeLabel"
placeholder="价格区间"
@click="openPricePicker"
></form-field>
/>
<!-- 查询按钮 -->
<view class="button-wrapper">
<!-- 搜索按钮 -->
<view class="search-button-wrapper">
<base-button
type="primary"
size="large"
text="查询"
text="搜索酒店"
block
@click="handleSearch"
></base-button>
/>
</view>
</base-card>
</view>
<!-- 酒店推荐 -->
<!-- 推荐列表 -->
<view class="recommend-section">
<view class="section-header">
<text class="section-title">酒店推荐</text>
<view class="section-more" @tap="handleSearch">
<text class="more-text">更多</text>
<u-icon name="arrow-right" :size="16" color="#999"></u-icon>
<text class="section-title">为你推荐</text>
<view class="section-action" @tap="handleSearch">
<text class="action-text">查看全部</text>
<u-icon name="arrow-right" :size="16" color="#999" />
</view>
</view>
<scroll-view
scroll-y
class="merchant-list"
@scrolltolower="loadMore"
@scrolltolower="handleLoadMore"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
:refresher-triggered="isRefreshing"
@refresherrefresh="handleRefresh"
>
<!-- 加载状态 -->
<loading-state v-if="loading && merchantList.length === 0" type="skeleton">
<!-- 骨架屏加载 -->
<loading-state v-if="isLoading && merchants.length === 0" type="skeleton">
<template #skeleton>
<view class="skeleton-merchant" v-for="i in 3" :key="i">
<view class="skeleton-item skeleton-image"></view>
<view class="skeleton-content">
<view class="skeleton-item skeleton-title"></view>
<view class="skeleton-item skeleton-text"></view>
<view class="skeleton-item skeleton-text short"></view>
<view v-for="i in 3" :key="i" class="skeleton-card">
<view class="skeleton-image" />
<view class="skeleton-body">
<view class="skeleton-title" />
<view class="skeleton-text" />
<view class="skeleton-text short" />
</view>
</view>
</template>
</loading-state>
<!-- 商家列表 -->
<view v-else class="list-content">
<!-- 商家卡片列表 -->
<view v-else class="merchant-list-content">
<merchant-card
v-for="merchant in merchantList"
v-for="merchant in displayMerchants"
:key="merchant.id"
:merchant="formatMerchant(merchant)"
@click="goMerchant(merchant.id)"
></merchant-card>
:merchant="merchant"
@click="handleMerchantClick(merchant.id)"
/>
</view>
<!-- 加载更多 -->
<loading-state v-if="loading && merchantList.length > 0" size="small"></loading-state>
<!-- 加载更多指示器 -->
<loading-state v-if="isLoading && merchants.length > 0" size="small" />
<!-- 没有更多 -->
<view v-if="!hasMore && merchantList.length > 0" class="no-more">
<text>没有更多了</text>
<!-- 加载完成提示 -->
<view v-if="!hasMoreData && merchants.length > 0" class="load-complete">
<text class="complete-text">已加载全部内容</text>
</view>
<!-- 空状态 -->
<empty-state
v-if="merchantList.length === 0 && !loading"
v-if="merchants.length === 0 && !isLoading"
type="search"
@action="onRefresh"
></empty-state>
@action="handleRefresh"
/>
</scroll-view>
</view>
<!-- 城市选择弹窗 -->
<CityPicker ref="citySelectorRef" :city="selectedCity" @change="onCityChange" />
<!-- 日期选择弹窗 -->
<DatePicker ref="datePickerRef" :check-in="checkInDate" :check-out="checkOutDate" @change="onDateChange" />
<!-- 房间人数弹窗 -->
<RoomGuestPicker ref="roomGuestRef" :room-count="roomCount" :adult-count="adultCount" :child-count="childCount" @change="onRoomGuestChange" />
<!-- 价格筛选弹窗 -->
<PricePicker ref="priceRef" :min="priceMin" :max="priceMax" @change="onPriceChange" />
<!-- 弹窗组件 -->
<CityPicker
ref="cityPickerRef"
:city="searchParams.city"
@change="handleCityChange"
/>
<DatePicker
ref="datePickerRef"
:check-in="searchParams.checkIn"
:check-out="searchParams.checkOut"
@change="handleDateChange"
/>
<RoomGuestPicker
ref="roomGuestPickerRef"
:room-count="searchParams.roomCount"
:adult-count="searchParams.adultCount"
:child-count="searchParams.childCount"
@change="handleRoomGuestChange"
/>
<PricePicker
ref="pricePickerRef"
:min="searchParams.minPrice"
:max="searchParams.maxPrice"
@change="handlePriceChange"
/>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onActivated } from 'vue';
import { ref, computed, onMounted, onActivated, reactive } from 'vue';
import { getMerchantList } from '@/api/user/merchant';
import { getDefaultDates, formatDateShort, getDateDescription, calculateNights } from '@/utils/date';
import type { Merchant, MerchantCardData, SearchParams } from '@/types/merchant';
import CityPicker from '@/components/CityPicker.vue';
import DatePicker from '@/components/DatePicker.vue';
import RoomGuestPicker from '@/components/RoomGuestPicker.vue';
@@ -148,208 +161,79 @@ import MerchantCard from '@/components/business/MerchantCard.vue';
import LoadingState from '@/components/base/LoadingState.vue';
import EmptyState from '@/components/base/EmptyState.vue';
// 默认日期:今天和明天
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
// 常量定义
const DEFAULT_CITY = '上海';
const PAGE_SIZE = 10;
const DEFAULT_COVER = '/static/default-avatar.png';
// Tab切换
const tabList = [
{ name: '国内酒店' },
{ name: '时租房' },
{ name: '公寓' }
];
const activeTab = ref(0);
// 获取默认日期
const { today, tomorrow } = getDefaultDates();
// 快捷入口
const quickEntries = [
{ icon: 'checkmark-circle', text: '签到' },
{ icon: 'coupon', text: '优惠券' },
{ icon: 'list', text: '住过看过' },
{ icon: 'shopping-cart', text: '华住商城' },
];
// 搜索参数
const searchParams = reactive<SearchParams>({
city: DEFAULT_CITY,
keyword: '',
checkIn: today,
checkOut: tomorrow,
roomCount: 1,
adultCount: 1,
childCount: 0,
minPrice: undefined,
maxPrice: undefined,
});
// 城市
const selectedCity = ref('上海');
const citySelectorRef = ref();
// 商家列表状态
const merchants = ref<Merchant[]>([]);
const currentPage = ref(1);
const isLoading = ref(false);
const isRefreshing = ref(false);
const hasMoreData = ref(true);
// 搜索关键字
const searchKeyword = ref('');
// 日期
const checkInDate = ref(fmt(today));
const checkOutDate = ref(fmt(tomorrow));
// 弹窗引用
const cityPickerRef = ref();
const datePickerRef = ref();
const roomGuestPickerRef = ref();
const pricePickerRef = ref();
const nightCount = computed(() => {
const diff = new Date(checkOutDate.value).getTime() - new Date(checkInDate.value).getTime();
return Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24)));
// 计算属性
const nightCount = computed(() =>
calculateNights(searchParams.checkIn, searchParams.checkOut)
);
const checkInLabel = computed(() =>
formatDateShort(searchParams.checkIn)
);
const checkOutLabel = computed(() =>
formatDateShort(searchParams.checkOut)
);
const checkInDesc = computed(() =>
getDateDescription(searchParams.checkIn)
);
const checkOutDesc = computed(() =>
getDateDescription(searchParams.checkOut)
);
const priceRangeLabel = computed(() => {
const { minPrice, maxPrice } = searchParams;
if (minPrice === undefined && maxPrice === undefined) return '';
if (minPrice !== undefined && maxPrice !== undefined) return `¥${minPrice}-${maxPrice}`;
if (minPrice !== undefined) return `¥${minPrice}+`;
return `¥${maxPrice}以下`;
});
const checkInLabel = computed(() => {
const d = new Date(checkInDate.value);
return `${d.getMonth() + 1}${d.getDate()}`;
});
const displayMerchants = computed<MerchantCardData[]>(() =>
merchants.value.map(transformMerchantData)
);
const checkOutLabel = computed(() => {
const d = new Date(checkOutDate.value);
return `${d.getMonth() + 1}${d.getDate()}`;
});
const checkInDesc = computed(() => getDateDesc(checkInDate.value));
const checkOutDesc = computed(() => getDateDesc(checkOutDate.value));
// 房间人数
const roomCount = ref(1);
const adultCount = ref(1);
const childCount = ref(0);
const roomGuestRef = ref();
// 价格
const priceMin = ref<number | undefined>(undefined);
const priceMax = ref<number | undefined>(undefined);
const priceRef = ref();
const priceLabel = computed(() => {
if (priceMin.value === undefined && priceMax.value === undefined) return '价格';
if (priceMin.value !== undefined && priceMax.value !== undefined) return `¥${priceMin.value}-${priceMax.value}`;
if (priceMin.value !== undefined) return `¥${priceMin.value}以上`;
return `¥${priceMax.value}以下`;
});
// 商家列表
const merchantList = ref<any[]>([]);
const page = ref(1);
const loading = ref(false);
const refreshing = ref(false);
const hasMore = ref(true);
function getDateDesc(dateStr: string): string {
const d = new Date(dateStr);
const today = new Date();
today.setHours(0, 0, 0, 0);
d.setHours(0, 0, 0, 0);
const diffDays = Math.round((d.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return '今天';
if (diffDays === 1) return '明天';
if (diffDays === 2) return '后天';
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekDays[d.getDay()];
}
async function fetchMerchants(reset = false) {
if (loading.value) return;
if (!reset && !hasMore.value) return;
loading.value = true;
if (reset) {
page.value = 1;
hasMore.value = true;
}
try {
const res = await getMerchantList({
page: page.value,
pageSize: 10,
});
const list = res.data?.list || [];
merchantList.value = reset ? list : [...merchantList.value, ...list];
hasMore.value = list.length >= 10;
page.value++;
} catch (e) {
console.error(e);
} finally {
loading.value = false;
refreshing.value = false;
}
}
function onCityChange(city: string) {
selectedCity.value = city;
}
function onDateChange(checkIn: string, checkOut: string) {
checkInDate.value = checkIn;
checkOutDate.value = checkOut;
}
function openCityPicker() {
citySelectorRef.value?.open();
}
function openDatePicker() {
datePickerRef.value?.open();
}
function openRoomGuestPicker() {
roomGuestRef.value?.open();
}
function openPricePicker() {
priceRef.value?.open();
}
function onRoomGuestChange(rooms: number, adults: number, children: number) {
roomCount.value = rooms;
adultCount.value = adults;
childCount.value = children;
}
function onPriceChange(min: number | undefined, max: number | undefined) {
priceMin.value = min;
priceMax.value = max;
}
function goLocationSearch() {
uni.navigateTo({ url: '/pages/location-search/index' });
}
function handleSearch() {
const params = [
`city=${encodeURIComponent(selectedCity.value)}`,
searchKeyword.value ? `keyword=${encodeURIComponent(searchKeyword.value)}` : '',
`checkIn=${checkInDate.value}`,
`checkOut=${checkOutDate.value}`,
`roomCount=${roomCount.value}`,
`adultCount=${adultCount.value}`,
`childCount=${childCount.value}`,
priceMin.value != null ? `minPrice=${priceMin.value}` : '',
priceMax.value != null ? `maxPrice=${priceMax.value}` : '',
].filter(Boolean).join('&');
uni.navigateTo({ url: `/pages/search/index?${params}` });
}
function setKeyword(keyword: string) {
searchKeyword.value = keyword;
}
function onRefresh() {
refreshing.value = true;
fetchMerchants(true);
}
function loadMore() {
fetchMerchants();
}
function goMerchant(id: number) {
const params = [
`id=${id}`,
`checkIn=${checkInDate.value}`,
`checkOut=${checkOutDate.value}`,
`roomCount=${roomCount.value}`,
`adultCount=${adultCount.value}`,
`childCount=${childCount.value}`,
].join('&');
uni.navigateTo({ url: `/pages/merchant-detail/index?${params}` });
}
// 格式化商家数据以适配 MerchantCard 组件
function formatMerchant(merchant: any) {
// 数据转换
function transformMerchantData(merchant: Merchant): MerchantCardData {
return {
id: merchant.id,
name: merchant.shopName,
coverImage: merchant.logo || '/static/default-avatar.png',
coverImage: merchant.logo || DEFAULT_COVER,
cityName: merchant.city,
rating: merchant.rating,
reviewCount: merchant.reviewCount,
@@ -360,27 +244,145 @@ function formatMerchant(merchant: any) {
distance: merchant.distance,
isRecommend: merchant.isRecommend,
isHot: merchant.isHot,
isNew: merchant.isNew
isNew: merchant.isNew,
};
}
onMounted(() => {
// 检查是否有位置搜索回传的关键字
const kw = (uni as any).__locationSearchKeyword;
if (kw) {
searchKeyword.value = kw;
(uni as any).__locationSearchKeyword = '';
// 获取商家列表
async function fetchMerchantList(shouldReset = false) {
if (isLoading.value) return;
if (!shouldReset && !hasMoreData.value) return;
isLoading.value = true;
if (shouldReset) {
currentPage.value = 1;
hasMoreData.value = true;
}
fetchMerchants(true);
try {
const response = await getMerchantList({
page: currentPage.value,
pageSize: PAGE_SIZE,
});
const list = response.data?.list || [];
merchants.value = shouldReset ? list : [...merchants.value, ...list];
hasMoreData.value = list.length >= PAGE_SIZE;
currentPage.value++;
} catch (error) {
console.error('获取商家列表失败:', error);
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
} finally {
isLoading.value = false;
isRefreshing.value = false;
}
}
// 弹窗操作
function openCityPicker() {
cityPickerRef.value?.open();
}
function openDatePicker() {
datePickerRef.value?.open();
}
function openPricePicker() {
pricePickerRef.value?.open();
}
// 弹窗回调
function handleCityChange(city: string) {
searchParams.city = city;
}
function handleDateChange(checkIn: string, checkOut: string) {
searchParams.checkIn = checkIn;
searchParams.checkOut = checkOut;
}
function handleRoomGuestChange(rooms: number, adults: number, children: number) {
searchParams.roomCount = rooms;
searchParams.adultCount = adults;
searchParams.childCount = children;
}
function handlePriceChange(min: number | undefined, max: number | undefined) {
searchParams.minPrice = min;
searchParams.maxPrice = max;
}
// 导航操作
function goLocationSearch() {
uni.navigateTo({ url: '/pages/location-search/index' });
}
function handleSearch() {
const queryParams = buildSearchQuery();
uni.navigateTo({ url: `/pages/search/index?${queryParams}` });
}
function handleMerchantClick(merchantId: number) {
const queryParams = buildMerchantQuery(merchantId);
uni.navigateTo({ url: `/pages/merchant-detail/index?${queryParams}` });
}
// 构建查询参数
function buildSearchQuery(): string {
const params = [
`city=${encodeURIComponent(searchParams.city)}`,
searchParams.keyword ? `keyword=${encodeURIComponent(searchParams.keyword)}` : '',
`checkIn=${searchParams.checkIn}`,
`checkOut=${searchParams.checkOut}`,
`roomCount=${searchParams.roomCount}`,
`adultCount=${searchParams.adultCount}`,
`childCount=${searchParams.childCount}`,
searchParams.minPrice !== undefined ? `minPrice=${searchParams.minPrice}` : '',
searchParams.maxPrice !== undefined ? `maxPrice=${searchParams.maxPrice}` : '',
];
return params.filter(Boolean).join('&');
}
function buildMerchantQuery(merchantId: number): string {
return [
`id=${merchantId}`,
`checkIn=${searchParams.checkIn}`,
`checkOut=${searchParams.checkOut}`,
`roomCount=${searchParams.roomCount}`,
`adultCount=${searchParams.adultCount}`,
`childCount=${searchParams.childCount}`,
].join('&');
}
// 列表操作
function handleRefresh() {
isRefreshing.value = true;
fetchMerchantList(true);
}
function handleLoadMore() {
fetchMerchantList(false);
}
// 处理位置搜索回传的关键词
function checkLocationSearchKeyword() {
const globalUni = uni as any;
const keyword = globalUni.__locationSearchKeyword;
if (keyword) {
searchParams.keyword = keyword;
globalUni.__locationSearchKeyword = '';
}
}
// 生命周期
onMounted(() => {
checkLocationSearchKeyword();
fetchMerchantList(true);
});
onActivated(() => {
// 页面激活时也检查(tabBar 切换回来时)
const kw = (uni as any).__locationSearchKeyword;
if (kw) {
searchKeyword.value = kw;
(uni as any).__locationSearchKeyword = '';
}
checkLocationSearchKeyword();
});
</script>
@@ -398,52 +400,64 @@ onActivated(() => {
/* ========== 搜索面板 ========== */
.search-panel {
padding: $spacing-xl;
background: linear-gradient(180deg, #f8f9fa 0%, $bg-page 100%);
}
.search-card {
overflow: visible;
border-radius: $radius-lg;
}
/* 日期选择器内容 */
/* 日期选择器 */
.date-picker-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: $spacing-sm 0;
gap: $spacing-md;
}
.date-section {
display: flex;
flex-direction: column;
gap: 4rpx;
gap: 6rpx;
flex: 1;
&:last-child {
align-items: flex-end;
}
}
.date-label {
font-size: $font-xs;
color: $text-tertiary;
line-height: 1.4;
}
.date-value {
font-size: $font-lg;
font-weight: $font-semibold;
color: $text-primary;
line-height: 1.3;
}
.nights-tag {
.nights-badge {
flex-shrink: 0;
font-size: $font-xs;
color: $primary-color;
background: $primary-50;
padding: 6rpx $spacing-md;
border-radius: $radius-sm;
font-weight: $font-medium;
background: linear-gradient(135deg, $primary-50 0%, rgba($primary-color, 0.08) 100%);
padding: 8rpx $spacing-md;
border-radius: $radius-round;
font-weight: $font-semibold;
border: 1rpx solid rgba($primary-color, 0.15);
}
.button-wrapper {
margin-top: $spacing-md;
.search-button-wrapper {
margin-top: $spacing-lg;
}
/* ========== 酒店推荐 ========== */
/* ========== 推荐区域 ========== */
.recommend-section {
flex: 1;
display: flex;
@@ -456,31 +470,34 @@ onActivated(() => {
display: flex;
justify-content: space-between;
align-items: center;
padding: $spacing-lg 0 $spacing-md;
padding: $spacing-xl 0 $spacing-md;
}
.section-title {
font-size: $font-lg;
font-weight: $font-semibold;
font-size: $font-xl;
font-weight: $font-bold;
color: $text-primary;
letter-spacing: 0.5rpx;
}
.section-more {
.section-action {
display: flex;
align-items: center;
gap: 4rpx;
font-size: $font-sm;
color: $text-secondary;
transition: $transition-color;
gap: 6rpx;
padding: $spacing-xs $spacing-sm;
border-radius: $radius-sm;
transition: $transition-all;
&:active {
color: $primary-color;
background: rgba($primary-color, 0.08);
transform: scale(0.98);
}
}
.more-text {
.action-text {
font-size: $font-sm;
color: $text-secondary;
font-weight: $font-medium;
}
/* ========== 商家列表 ========== */
@@ -489,52 +506,91 @@ onActivated(() => {
height: 0;
}
.list-content {
padding-bottom: $spacing-xl;
.merchant-list-content {
padding-bottom: $spacing-2xl;
}
/* 骨架屏 */
.skeleton-merchant {
.skeleton-card {
display: flex;
gap: $spacing-md;
padding: $spacing-xl;
padding: $spacing-lg;
background: $bg-card;
border-radius: $radius-md;
border-radius: $radius-lg;
margin-bottom: $spacing-md;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.skeleton-image {
width: 200rpx;
height: 200rpx;
flex-shrink: 0;
border-radius: $radius-md;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-content {
.skeleton-body {
flex: 1;
display: flex;
flex-direction: column;
gap: $spacing-sm;
gap: $spacing-md;
justify-content: center;
}
.skeleton-title,
.skeleton-text {
height: 32rpx;
border-radius: $radius-sm;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
}
.skeleton-title {
width: 60%;
width: 70%;
height: 36rpx;
}
.skeleton-text {
width: 100%;
height: 28rpx;
&.short {
width: 80%;
width: 60%;
}
}
/* 没有更多 */
.no-more {
text-align: center;
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 加载完成提示 */
.load-complete {
display: flex;
align-items: center;
justify-content: center;
padding: $spacing-2xl 0;
position: relative;
&::before,
&::after {
content: '';
flex: 1;
height: 1rpx;
background: linear-gradient(to right, transparent, $border-light, transparent);
}
}
.complete-text {
padding: 0 $spacing-xl;
font-size: $font-xs;
color: $text-placeholder;
font-size: $font-sm;
white-space: nowrap;
}
</style>
File diff suppressed because it is too large Load Diff
+556 -217
View File
@@ -1,88 +1,196 @@
<template>
<view class="page-order-create">
<!-- 房源信息 -->
<!-- 房源信息卡片 -->
<view class="room-card" v-if="room">
<image class="room-cover" :src="room.coverImage || '/static/default-room.png'" mode="aspectFill" />
<view class="room-cover-wrapper">
<image class="room-cover" :src="room.coverImage || '/static/default-room.png'" mode="aspectFill" />
<view class="room-badge">精选</view>
</view>
<view class="room-info">
<text class="room-name">{{ room.name }}</text>
<tag-list v-if="roomTags.length" :tags="roomTags" type="primary" size="small" />
<price-tag :price="room.price" size="medium" unit="/晚" />
<view class="room-price-row">
<price-tag :price="room.price" size="medium" unit="/晚" />
<text class="room-stock-tip" v-if="maxRoomCount <= 3">仅剩{{ maxRoomCount }}</text>
</view>
</view>
</view>
<!-- 入住信息 -->
<view class="section">
<view class="section-title">入住信息</view>
<view class="date-row" @tap="openCalendarPicker">
<view class="date-item">
<text class="date-label">入住</text>
<text class="date-value">{{ checkInLabel }}</text>
<view class="section-header">
<view class="section-icon">📅</view>
<text class="section-title">入住信息</text>
</view>
<view class="date-selector" @tap="openCalendarPicker">
<view class="date-box">
<text class="date-label">入住日期</text>
<text class="date-value">{{ checkInLabel || '请选择' }}</text>
<text class="date-weekday">{{ checkInWeekday }}</text>
</view>
<view class="date-item">
<text class="date-label">离店</text>
<text class="date-value">{{ checkOutLabel }}</text>
<view class="date-divider">
<view class="divider-line"></view>
<text class="divider-icon"></text>
<view class="divider-line"></view>
</view>
<view class="date-box">
<text class="date-label">离店日期</text>
<text class="date-value">{{ checkOutLabel || '请选择' }}</text>
<text class="date-weekday">{{ checkOutWeekday }}</text>
</view>
<view class="night-badge">
<text class="night-text">{{ nightCount }}</text>
<text class="night-text">{{ nightCount }}</text>
</view>
</view>
<!-- 房间数选择 -->
<view class="count-row">
<view class="count-item">
<text class="count-label">房间套数</text>
<!-- 房间和人数选择 -->
<view class="count-section">
<view class="count-row">
<view class="count-left">
<text class="count-icon">🏠</text>
<view class="count-text-group">
<text class="count-label">房间套数</text>
<text class="count-sublabel">最多{{ maxRoomCount }}套可订</text>
</view>
</view>
<view class="count-stepper">
<text class="stepper-btn" :class="{ disabled: roomCount <= 1 }" @tap="decreaseRoomCount">-</text>
<view class="stepper-btn" :class="{ disabled: roomCount <= 1 }" @tap="decreaseRoomCount">
<text class="stepper-icon">-</text>
</view>
<text class="stepper-value">{{ roomCount }}</text>
<text class="stepper-btn" :class="{ disabled: roomCount >= maxRoomCount }" @tap="increaseRoomCount">+</text>
<view class="stepper-btn" :class="{ disabled: roomCount >= maxRoomCount }" @tap="increaseRoomCount">
<text class="stepper-icon">+</text>
</view>
</view>
<text class="count-tip">最多{{ maxRoomCount }}</text>
</view>
</view>
<!-- 入住人数选择 -->
<view class="count-row">
<view class="count-item">
<text class="count-label">入住人数</text>
<view class="count-divider"></view>
<view class="count-row">
<view class="count-left">
<text class="count-icon">👥</text>
<view class="count-text-group">
<text class="count-label">入住人数</text>
<text class="count-sublabel">最多{{ maxGuestCount }}</text>
</view>
</view>
<view class="count-stepper">
<text class="stepper-btn" :class="{ disabled: guestCount <= 1 }" @tap="decreaseGuestCount">-</text>
<view class="stepper-btn" :class="{ disabled: guestCount <= 1 }" @tap="decreaseGuestCount">
<text class="stepper-icon">-</text>
</view>
<text class="stepper-value">{{ guestCount }}</text>
<text class="stepper-btn" :class="{ disabled: guestCount >= maxGuestCount }" @tap="increaseGuestCount">+</text>
<view class="stepper-btn" :class="{ disabled: guestCount >= maxGuestCount }" @tap="increaseGuestCount">
<text class="stepper-icon">+</text>
</view>
</view>
<text class="count-tip">最多{{ maxGuestCount }}</text>
</view>
</view>
<view class="price-summary">
<text class="summary-label">房费合计</text>
<price-tag :price="totalPrice" size="large" type="primary" />
<!-- 价格明细 -->
<view class="price-detail">
<view class="price-row">
<text class="price-label">房费 (¥{{ room?.price }} × {{ nightCount }} × {{ roomCount }})</text>
<text class="price-value">¥{{ totalPrice }}</text>
</view>
<view class="price-divider"></view>
<view class="price-row total">
<text class="price-label">合计</text>
<view class="price-total">
<text class="price-symbol">¥</text>
<text class="price-amount">{{ totalPrice }}</text>
</view>
</view>
</view>
</view>
<!-- 联系人信息 -->
<view class="section">
<view class="section-title">联系人信息</view>
<form-field label="姓名" required>
<input class="form-input" v-model="contactName" placeholder="请输入入住人姓名" />
</form-field>
<form-field label="手机号" required>
<input class="form-input" v-model="contactPhone" type="number" placeholder="请输入手机号" maxlength="11" />
</form-field>
<form-field label="身份证号">
<input class="form-input" v-model="contactIdCard" placeholder="请输入身份证号(选填)" maxlength="18" />
</form-field>
<form-field label="备注">
<textarea class="form-textarea" v-model="remark" placeholder="如有特殊需求请备注" />
</form-field>
<view class="section-header">
<view class="section-icon">👤</view>
<text class="section-title">联系人信息</text>
</view>
<view class="form-group">
<view class="form-item">
<view class="form-label-row">
<text class="form-label">入住人姓名</text>
<text class="form-required">*</text>
</view>
<input
class="form-input"
v-model="contactName"
placeholder="请输入真实姓名"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-item">
<view class="form-label-row">
<text class="form-label">手机号码</text>
<text class="form-required">*</text>
</view>
<input
class="form-input"
v-model="contactPhone"
type="number"
placeholder="用于接收订单通知"
maxlength="11"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-item">
<view class="form-label-row">
<text class="form-label">身份证号</text>
<text class="form-optional">选填</text>
</view>
<input
class="form-input"
v-model="contactIdCard"
placeholder="入住时需提供"
maxlength="18"
placeholder-class="input-placeholder"
/>
</view>
<view class="form-item">
<view class="form-label-row">
<text class="form-label">特殊需求</text>
<text class="form-optional">选填</text>
</view>
<textarea
class="form-textarea"
v-model="remark"
placeholder="如需加床、禁烟房等,请在此备注"
placeholder-class="input-placeholder"
/>
</view>
</view>
</view>
<!-- 提交按钮 -->
<view class="submit-bar">
<view class="total-row">
<text class="total-label">应付金额</text>
<price-tag :price="totalPrice" size="large" type="primary" />
<!-- 温馨提示 -->
<view class="tips-section">
<view class="tips-title">📌 温馨提示</view>
<view class="tips-content">
<text class="tips-item"> 请确保入住人信息真实有效</text>
<text class="tips-item"> 入住时需出示有效身份证件</text>
<text class="tips-item"> 如需取消订单请提前联系商家</text>
</view>
</view>
<!-- 底部提交栏 -->
<view class="submit-bar">
<view class="submit-left">
<text class="submit-label">应付金额</text>
<view class="submit-price">
<text class="submit-symbol">¥</text>
<text class="submit-amount">{{ totalPrice }}</text>
</view>
</view>
<view class="submit-btn" @tap="handleSubmit">
<text class="submit-text">提交订单</text>
</view>
<base-button type="primary" size="large" @click="handleSubmit">提交订单</base-button>
</view>
<!-- 日期选择弹窗 -->
@@ -101,10 +209,8 @@ import { ref, computed, onMounted, watch } from 'vue';
import { getRoomDetail, getRoomCalendar } from '@/api/user/room';
import { createOrder } from '@/api/user/order';
import RoomCalendarPicker from '@/components/RoomCalendarPicker.vue';
import BaseButton from '@/components/base/BaseButton.vue';
import PriceTag from '@/components/business/PriceTag.vue';
import TagList from '@/components/business/TagList.vue';
import FormField from '@/components/business/FormField.vue';
const roomId = ref(0);
const merchantId = ref(0);
@@ -130,6 +236,8 @@ const typeLabels: Record<string, string> = {
hostel: '青旅',
};
const weekdayLabels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
// 房源标签
const roomTags = computed(() => {
const tags = [];
@@ -160,6 +268,18 @@ const checkOutLabel = computed(() => {
return `${d.getMonth() + 1}${d.getDate()}`;
});
const checkInWeekday = computed(() => {
if (!checkInDate.value) return '';
const d = new Date(checkInDate.value);
return weekdayLabels[d.getDay()];
});
const checkOutWeekday = computed(() => {
if (!checkOutDate.value) return '';
const d = new Date(checkOutDate.value);
return weekdayLabels[d.getDay()];
});
// 最大房间套数:取入住日期区间内每日最小剩余库存
const maxRoomCount = computed(() => {
if (!checkInDate.value || !checkOutDate.value || calendarStock.value.size === 0) return 1;
@@ -316,295 +436,514 @@ function onDateChange(checkIn: string, checkOut: string) {
.page-order-create {
min-height: 100vh;
background: $bg-page;
padding-bottom: 120rpx;
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
padding-bottom: 160rpx;
}
// 房源卡片
.room-card {
display: flex;
background: $bg-card;
margin: $spacing-xl;
border-radius: $radius-lg;
padding: $spacing-xl;
border: 1rpx solid $border-light;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
margin: 24rpx;
border-radius: 24rpx;
padding: 24rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: linear-gradient(90deg, $primary-color 0%, #ff6b9d 100%);
}
}
.room-cover-wrapper {
position: relative;
flex-shrink: 0;
}
.room-cover {
width: 160rpx;
height: 160rpx;
border-radius: $radius-base;
flex-shrink: 0;
width: 180rpx;
height: 180rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.room-badge {
position: absolute;
top: 8rpx;
left: 8rpx;
background: linear-gradient(135deg, $primary-color 0%, #ff6b9d 100%);
color: #ffffff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 600;
box-shadow: 0 2rpx 8rpx rgba(255, 107, 157, 0.3);
}
.room-info {
flex: 1;
margin-left: $spacing-lg;
margin-left: 24rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.room-name {
font-size: $font-md;
font-weight: $font-semibold;
font-size: 32rpx;
font-weight: 700;
color: $text-primary;
line-height: 1.4;
margin-bottom: 12rpx;
}
.room-tags {
.room-price-row {
display: flex;
gap: $spacing-xs;
align-items: center;
justify-content: space-between;
}
.tag {
font-size: $font-xs;
color: $primary-color;
background: $primary-bg;
padding: 4rpx $spacing-sm;
border-radius: $radius-xs;
}
.room-price {
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: $font-sm;
color: $primary-color;
}
.price-value {
font-size: $font-xl;
font-weight: $font-bold;
color: $primary-color;
}
.price-unit {
font-size: $font-xs;
color: $text-secondary;
margin-left: 4rpx;
.room-stock-tip {
font-size: 22rpx;
color: #ff4d4f;
background: #fff1f0;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: 500;
}
// 通用区块
.section {
background: $bg-card;
margin: $spacing-xl;
border-radius: $radius-lg;
padding: $spacing-xl;
border: 1rpx solid $border-light;
background: #ffffff;
margin: 24rpx;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.04);
}
.section-header {
display: flex;
align-items: center;
margin-bottom: 32rpx;
}
.section-icon {
font-size: 40rpx;
margin-right: 12rpx;
}
.section-title {
font-size: $font-base;
font-weight: $font-semibold;
font-size: 32rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: $spacing-lg;
}
.date-row {
// 日期选择器
.date-selector {
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-radius: 20rpx;
padding: 32rpx 24rpx;
display: flex;
align-items: center;
padding: $spacing-md 0;
border-bottom: 1rpx solid $border-light;
position: relative;
border: 2rpx solid #e8e8e8;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
}
.date-item {
.date-box {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.date-label {
font-size: 24rpx;
color: $text-secondary;
margin-bottom: 8rpx;
}
.date-value {
font-size: 32rpx;
font-weight: 700;
color: $text-primary;
margin-bottom: 4rpx;
}
.date-weekday {
font-size: 22rpx;
color: $text-tertiary;
}
.date-divider {
display: flex;
flex-direction: column;
align-items: center;
margin: 0 16rpx;
}
.divider-line {
width: 1rpx;
height: 20rpx;
background: #e8e8e8;
}
.divider-icon {
font-size: 28rpx;
color: $primary-color;
margin: 4rpx 0;
}
.night-badge {
position: absolute;
top: -12rpx;
right: 24rpx;
background: linear-gradient(135deg, $primary-color 0%, #ff6b9d 100%);
padding: 8rpx 20rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.3);
}
.night-text {
font-size: 24rpx;
font-weight: 700;
color: #ffffff;
}
// 数量选择
.count-section {
margin-top: 24rpx;
background: #f8f9fa;
border-radius: 20rpx;
padding: 8rpx 0;
}
.count-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 24rpx;
}
.count-left {
display: flex;
align-items: center;
flex: 1;
}
.count-icon {
font-size: 40rpx;
margin-right: 16rpx;
}
.count-text-group {
display: flex;
flex-direction: column;
}
.date-label {
font-size: $font-sm;
color: $text-secondary;
}
.date-value {
font-size: $font-lg;
font-weight: $font-semibold;
color: $text-primary;
margin-top: 4rpx;
}
.night-badge {
background: $primary-bg;
padding: $spacing-xs $spacing-md;
border-radius: $radius-sm;
}
.night-text {
font-size: $font-sm;
color: $primary-color;
}
.count-row {
padding: $spacing-md 0;
border-bottom: 1rpx solid $border-light;
}
.count-item {
display: flex;
align-items: center;
}
.count-label {
font-size: $font-sm;
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
flex-shrink: 0;
width: 140rpx;
margin-bottom: 4rpx;
}
.count-sublabel {
font-size: 22rpx;
color: $text-tertiary;
}
.count-divider {
height: 1rpx;
background: #e8e8e8;
margin: 0 24rpx;
}
.count-stepper {
display: flex;
align-items: center;
margin-left: $spacing-md;
background: #ffffff;
border-radius: 16rpx;
padding: 4rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.stepper-btn {
width: 56rpx;
height: 56rpx;
border-radius: $radius-sm;
background: $bg-page;
border: 1rpx solid $border-base;
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
background: linear-gradient(135deg, $primary-color 0%, #ff6b9d 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: $font-lg;
color: $text-primary;
transition: all 0.2s ease;
&:active {
transform: scale(0.9);
}
&.disabled {
background: #f0f0f0;
opacity: 0.5;
}
}
.stepper-btn:active {
background: $bg-hover;
}
.stepper-btn.disabled {
color: $text-disabled;
background: $bg-disabled;
border-color: $border-light;
.stepper-icon {
font-size: 36rpx;
font-weight: 700;
color: #ffffff;
}
.stepper-value {
min-width: 60rpx;
min-width: 80rpx;
text-align: center;
font-size: $font-lg;
font-weight: $font-semibold;
font-size: 32rpx;
font-weight: 700;
color: $text-primary;
}
.count-tip {
font-size: $font-xs;
color: $text-secondary;
margin-left: $spacing-md;
// 价格明细
.price-detail {
margin-top: 32rpx;
background: linear-gradient(135deg, #fff7e6 0%, #fff1f0 100%);
border-radius: 20rpx;
padding: 24rpx;
}
.price-summary {
.price-row {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: $spacing-md;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
&.total {
margin-top: 16rpx;
padding-top: 16rpx;
}
}
.summary-label {
font-size: $font-sm;
.price-label {
font-size: 26rpx;
color: $text-secondary;
}
.summary-value {
font-size: $font-xl;
font-weight: $font-bold;
.price-value {
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
}
.price-divider {
height: 1rpx;
background: rgba(0, 0, 0, 0.06);
margin: 16rpx 0;
}
.price-total {
display: flex;
align-items: baseline;
}
.price-symbol {
font-size: 28rpx;
font-weight: 700;
color: $primary-color;
margin-right: 4rpx;
}
.price-amount {
font-size: 40rpx;
font-weight: 700;
color: $primary-color;
}
// 表单
.form-group {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.form-item {
display: flex;
flex-direction: column;
margin-bottom: $spacing-lg;
}
.form-label-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.form-label {
font-size: $font-sm;
font-size: 28rpx;
font-weight: 600;
color: $text-primary;
margin-bottom: $spacing-xs;
}
.form-required {
font-size: 28rpx;
color: #ff4d4f;
margin-left: 4rpx;
}
.form-optional {
font-size: 22rpx;
color: $text-tertiary;
margin-left: 8rpx;
background: #f0f0f0;
padding: 2rpx 8rpx;
border-radius: 4rpx;
}
.form-input {
height: 80rpx;
background: $bg-card;
border: 1rpx solid $border-base;
border-radius: $radius-sm;
padding: 0 $spacing-md;
font-size: $font-base;
height: 88rpx;
background: #f8f9fa;
border: 2rpx solid #e8e8e8;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: $text-primary;
transition: all 0.2s ease;
}
transition: all 0.3s ease;
.form-input:focus {
border-color: $primary-color;
&:focus {
background: #ffffff;
border-color: $primary-color;
box-shadow: 0 0 0 4rpx rgba(255, 107, 157, 0.1);
}
}
.form-textarea {
height: 120rpx;
background: $bg-card;
border: 1rpx solid $border-base;
border-radius: $radius-sm;
padding: $spacing-md;
font-size: $font-base;
min-height: 160rpx;
background: #f8f9fa;
border: 2rpx solid #e8e8e8;
border-radius: 16rpx;
padding: 20rpx 24rpx;
font-size: 28rpx;
color: $text-primary;
transition: all 0.2s ease;
line-height: 1.6;
transition: all 0.3s ease;
&:focus {
background: #ffffff;
border-color: $primary-color;
box-shadow: 0 0 0 4rpx rgba(255, 107, 157, 0.1);
}
}
.form-textarea:focus {
border-color: $primary-color;
.input-placeholder {
color: #bfbfbf;
}
// 温馨提示
.tips-section {
background: linear-gradient(135deg, #e6f7ff 0%, #f0f5ff 100%);
margin: 24rpx;
border-radius: 20rpx;
padding: 24rpx;
border-left: 6rpx solid #1890ff;
}
.tips-title {
font-size: 28rpx;
font-weight: 700;
color: #1890ff;
margin-bottom: 16rpx;
}
.tips-content {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.tips-item {
font-size: 24rpx;
color: #595959;
line-height: 1.6;
}
// 底部提交栏
.submit-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: $bg-card;
padding: $spacing-xl;
border-top: 1rpx solid $border-light;
background: #ffffff;
padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
}
.total-row {
.submit-left {
flex: 1;
display: flex;
align-items: center;
flex-direction: column;
}
.total-label {
font-size: $font-sm;
.submit-label {
font-size: 24rpx;
color: $text-secondary;
margin-bottom: 4rpx;
}
.total-value {
font-size: $font-2xl;
font-weight: $font-bold;
.submit-price {
display: flex;
align-items: baseline;
}
.submit-symbol {
font-size: 28rpx;
font-weight: 700;
color: $primary-color;
margin-right: 4rpx;
}
.submit-amount {
font-size: 48rpx;
font-weight: 700;
color: $primary-color;
margin-left: $spacing-xs;
}
.submit-btn {
background: $primary-color;
border-radius: $radius-base;
height: 88rpx;
width: 240rpx;
background: linear-gradient(135deg, $primary-color 0%, #ff6b9d 100%);
border-radius: 48rpx;
height: 96rpx;
width: 280rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
box-shadow: 0 8rpx 24rpx rgba(255, 107, 157, 0.4);
transition: all 0.3s ease;
.submit-btn:active {
background: $primary-dark;
&:active {
transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.3);
}
}
.submit-text {
font-size: $font-lg;
font-weight: $font-bold;
color: #FFFFFF;
font-size: 32rpx;
font-weight: 700;
color: #ffffff;
letter-spacing: 2rpx;
}
</style>
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
export interface Merchant {
id: number;
sellerId: number;
shopName: string;
logo: string;
description: string;
phone: string;
province: string;
city: string;
district: string;
address: string;
longitude: number;
latitude: number;
businessLicense: string;
licenseNo: string;
legalPerson: string;
status: 'pending' | 'approved' | 'rejected' | 'frozen';
rejectReason: string;
deposit: number;
rating: number;
reviewCount: number;
autoConfirm: boolean;
createdAt: string;
updatedAt: string;
// 扩展字段
minPrice?: number;
roomCount?: number;
distance?: number;
isRecommend?: boolean;
isHot?: boolean;
isNew?: boolean;
facilities?: string[];
}
export interface MerchantCardData {
id: number;
name: string;
coverImage: string;
cityName: string;
rating: number;
reviewCount: number;
description: string;
facilities: string[];
minPrice: number;
roomCount?: number;
distance?: number;
isRecommend?: boolean;
isHot?: boolean;
isNew?: boolean;
}
export interface SearchParams {
city: string;
keyword?: string;
checkIn: string;
checkOut: string;
roomCount: number;
adultCount: number;
childCount: number;
minPrice?: number;
maxPrice?: number;
}
+77
View File
@@ -0,0 +1,77 @@
export interface Room {
id: number;
merchantId: number;
name: string;
type: string;
coverImage: string;
images: string[];
description: string;
area: number;
bedType: string;
maxGuests: number;
price: number;
weekendPrice: number;
facilities: string[];
status: 'available' | 'unavailable' | 'maintenance';
floor: string;
roomNumber: string;
createdAt: string;
updatedAt: string;
// 扩展字段
isAvailable?: boolean;
currentPrice?: number;
nights?: number;
}
export interface RoomCardData {
id: number;
name: string;
coverImage: string;
type: string;
area: number;
bedType: string;
maxGuests: number;
price: number;
currentPrice?: number;
facilities: string[];
isAvailable: boolean;
floor?: string;
roomNumber?: string;
}
export interface Review {
id: number;
userId: number;
orderId: number;
merchantId: number;
roomId: number;
rating: number;
content: string;
images: string[];
isAnonymous: boolean;
merchantReply: string;
createdAt: string;
updatedAt: string;
// 扩展字段
userAvatar?: string;
userName?: string;
}
export interface ReviewCardData {
id: number;
userName: string;
userAvatar: string;
rating: number;
content: string;
images: string[];
reply: string;
createdAt: string;
}
export interface BookingParams {
checkIn: string;
checkOut: string;
roomCount: number;
adultCount: number;
childCount: number;
}
+66
View File
@@ -0,0 +1,66 @@
/**
* 日期工具函数
*/
/**
* 格式化日期为 YYYY-MM-DD
*/
export function formatDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 格式化日期为 M月D日
*/
export function formatDateShort(date: Date | string): string {
const d = typeof date === 'string' ? new Date(date) : date;
return `${d.getMonth() + 1}${d.getDate()}`;
}
/**
* 获取日期描述(今天、明天、后天、周X)
*/
export function getDateDescription(dateStr: string): string {
const targetDate = new Date(dateStr);
const today = new Date();
// 重置时间为 00:00:00 以便比较日期
today.setHours(0, 0, 0, 0);
targetDate.setHours(0, 0, 0, 0);
const diffDays = Math.round((targetDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
if (diffDays === 0) return '今天';
if (diffDays === 1) return '明天';
if (diffDays === 2) return '后天';
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return weekDays[targetDate.getDay()];
}
/**
* 计算两个日期之间的天数
*/
export function calculateNights(checkIn: string, checkOut: string): number {
const checkInDate = new Date(checkIn);
const checkOutDate = new Date(checkOut);
const diffTime = checkOutDate.getTime() - checkInDate.getTime();
return Math.max(1, Math.ceil(diffTime / (1000 * 60 * 60 * 24)));
}
/**
* 获取今天和明天的日期
*/
export function getDefaultDates(): { today: string; tomorrow: string } {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
return {
today: formatDate(today),
tomorrow: formatDate(tomorrow),
};
}