dev
This commit is contained in:
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user