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> <template>
<view class="page-index"> <view class="page-index">
<!-- 搜索面板 - 使用毛玻璃效果 --> <!-- 搜索面板 -->
<view class="search-panel"> <view class="search-panel">
<base-card class="search-card" variant="elevated" :shadow="false"> <base-card class="search-card" variant="elevated" :shadow="false">
<!-- 位置 --> <!-- 城市选择 -->
<form-field <form-field
type="select" type="select"
:display-value="selectedCity" :display-value="searchParams.city"
placeholder="选择城市" placeholder="选择目的地城市"
right-icon="map" right-icon="map"
:show-select-arrow="false" :show-select-arrow="false"
@click="openCityPicker" @click="openCityPicker"
></form-field> />
<!-- 日期 --> <!-- 入住离店日期 -->
<form-field <form-field type="custom" @click="openDatePicker">
type="custom"
@click="openDatePicker"
>
<view class="date-picker-content"> <view class="date-picker-content">
<view class="date-section"> <view class="date-section">
<text class="date-label">{{ checkInDesc }}入住</text> <text class="date-label">{{ checkInDesc }}入住</text>
<text class="date-value">{{ checkInLabel }}</text> <text class="date-value">{{ checkInLabel }}</text>
</view> </view>
<view class="nights-tag">{{ nightCount }}</view> <view class="nights-badge">{{ nightCount }}</view>
<view class="date-section"> <view class="date-section">
<text class="date-label">{{ checkOutDesc }}离店</text> <text class="date-label">{{ checkOutDesc }}离店</text>
<text class="date-value">{{ checkOutLabel }}</text> <text class="date-value">{{ checkOutLabel }}</text>
@@ -31,112 +28,128 @@
</view> </view>
</form-field> </form-field>
<!-- 关键词 --> <!-- 搜索关键词 -->
<form-field <form-field
type="select" type="select"
:display-value="searchKeyword" :display-value="searchParams.keyword"
placeholder="输入关键词搜索酒店" placeholder="搜索酒店、民宿、位置"
right-icon="search" right-icon="search"
@click="goLocationSearch" @click="goLocationSearch"
></form-field> />
<!-- 价格筛选 --> <!-- 价格区间 -->
<form-field <form-field
type="select" type="select"
:display-value="priceLabel !== '价格' ? priceLabel : ''" :display-value="priceRangeLabel"
placeholder="价格" placeholder="价格区间"
@click="openPricePicker" @click="openPricePicker"
></form-field> />
<!-- 查询按钮 --> <!-- 搜索按钮 -->
<view class="button-wrapper"> <view class="search-button-wrapper">
<base-button <base-button
type="primary" type="primary"
size="large" size="large"
text="查询" text="搜索酒店"
block block
@click="handleSearch" @click="handleSearch"
></base-button> />
</view> </view>
</base-card> </base-card>
</view> </view>
<!-- 酒店推荐 --> <!-- 推荐列表 -->
<view class="recommend-section"> <view class="recommend-section">
<view class="section-header"> <view class="section-header">
<text class="section-title">酒店推荐</text> <text class="section-title">为你推荐</text>
<view class="section-more" @tap="handleSearch"> <view class="section-action" @tap="handleSearch">
<text class="more-text">更多</text> <text class="action-text">查看全部</text>
<u-icon name="arrow-right" :size="16" color="#999"></u-icon> <u-icon name="arrow-right" :size="16" color="#999" />
</view> </view>
</view> </view>
<scroll-view <scroll-view
scroll-y scroll-y
class="merchant-list" class="merchant-list"
@scrolltolower="loadMore" @scrolltolower="handleLoadMore"
refresher-enabled refresher-enabled
:refresher-triggered="refreshing" :refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh" @refresherrefresh="handleRefresh"
> >
<!-- 加载状态 --> <!-- 骨架屏加载 -->
<loading-state v-if="loading && merchantList.length === 0" type="skeleton"> <loading-state v-if="isLoading && merchants.length === 0" type="skeleton">
<template #skeleton> <template #skeleton>
<view class="skeleton-merchant" v-for="i in 3" :key="i"> <view v-for="i in 3" :key="i" class="skeleton-card">
<view class="skeleton-item skeleton-image"></view> <view class="skeleton-image" />
<view class="skeleton-content"> <view class="skeleton-body">
<view class="skeleton-item skeleton-title"></view> <view class="skeleton-title" />
<view class="skeleton-item skeleton-text"></view> <view class="skeleton-text" />
<view class="skeleton-item skeleton-text short"></view> <view class="skeleton-text short" />
</view> </view>
</view> </view>
</template> </template>
</loading-state> </loading-state>
<!-- 商家列表 --> <!-- 商家卡片列表 -->
<view v-else class="list-content"> <view v-else class="merchant-list-content">
<merchant-card <merchant-card
v-for="merchant in merchantList" v-for="merchant in displayMerchants"
:key="merchant.id" :key="merchant.id"
:merchant="formatMerchant(merchant)" :merchant="merchant"
@click="goMerchant(merchant.id)" @click="handleMerchantClick(merchant.id)"
></merchant-card> />
</view> </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"> <view v-if="!hasMoreData && merchants.length > 0" class="load-complete">
<text>没有更多了</text> <text class="complete-text">已加载全部内容</text>
</view> </view>
<!-- 空状态 --> <!-- 空状态 -->
<empty-state <empty-state
v-if="merchantList.length === 0 && !loading" v-if="merchants.length === 0 && !isLoading"
type="search" type="search"
@action="onRefresh" @action="handleRefresh"
></empty-state> />
</scroll-view> </scroll-view>
</view> </view>
<!-- 城市选择弹窗 --> <!-- 弹窗组件 -->
<CityPicker ref="citySelectorRef" :city="selectedCity" @change="onCityChange" /> <CityPicker
ref="cityPickerRef"
<!-- 日期选择弹窗 --> :city="searchParams.city"
<DatePicker ref="datePickerRef" :check-in="checkInDate" :check-out="checkOutDate" @change="onDateChange" /> @change="handleCityChange"
/>
<!-- 房间人数弹窗 --> <DatePicker
<RoomGuestPicker ref="roomGuestRef" :room-count="roomCount" :adult-count="adultCount" :child-count="childCount" @change="onRoomGuestChange" /> ref="datePickerRef"
:check-in="searchParams.checkIn"
<!-- 价格筛选弹窗 --> :check-out="searchParams.checkOut"
<PricePicker ref="priceRef" :min="priceMin" :max="priceMax" @change="onPriceChange" /> @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> </view>
</template> </template>
<script setup lang="ts"> <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 { 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 CityPicker from '@/components/CityPicker.vue';
import DatePicker from '@/components/DatePicker.vue'; import DatePicker from '@/components/DatePicker.vue';
import RoomGuestPicker from '@/components/RoomGuestPicker.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 LoadingState from '@/components/base/LoadingState.vue';
import EmptyState from '@/components/base/EmptyState.vue'; import EmptyState from '@/components/base/EmptyState.vue';
// 默认日期:今天和明天 // 常量定义
const today = new Date(); const DEFAULT_CITY = '上海';
const tomorrow = new Date(today); const PAGE_SIZE = 10;
tomorrow.setDate(tomorrow.getDate() + 1); const DEFAULT_COVER = '/static/default-avatar.png';
const fmt = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
// Tab切换 // 获取默认日期
const tabList = [ const { today, tomorrow } = getDefaultDates();
{ name: '国内酒店' },
{ name: '时租房' },
{ name: '公寓' }
];
const activeTab = ref(0);
// 快捷入口 // 搜索参数
const quickEntries = [ const searchParams = reactive<SearchParams>({
{ icon: 'checkmark-circle', text: '签到' }, city: DEFAULT_CITY,
{ icon: 'coupon', text: '优惠券' }, keyword: '',
{ icon: 'list', text: '住过看过' }, checkIn: today,
{ icon: 'shopping-cart', text: '华住商城' }, checkOut: tomorrow,
]; roomCount: 1,
adultCount: 1,
childCount: 0,
minPrice: undefined,
maxPrice: undefined,
});
// 城市 // 商家列表状态
const selectedCity = ref('上海'); const merchants = ref<Merchant[]>([]);
const citySelectorRef = ref(); const currentPage = ref(1);
const isLoading = ref(false);
const isRefreshing = ref(false);
const hasMoreData = ref(true);
// 搜索关键字 // 弹窗引用
const searchKeyword = ref(''); const cityPickerRef = ref();
// 日期
const checkInDate = ref(fmt(today));
const checkOutDate = ref(fmt(tomorrow));
const datePickerRef = 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(); const nightCount = computed(() =>
return Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24))); 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 displayMerchants = computed<MerchantCardData[]>(() =>
const d = new Date(checkInDate.value); merchants.value.map(transformMerchantData)
return `${d.getMonth() + 1}${d.getDate()}`; );
});
const checkOutLabel = computed(() => { // 数据转换
const d = new Date(checkOutDate.value); function transformMerchantData(merchant: Merchant): MerchantCardData {
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) {
return { return {
id: merchant.id, id: merchant.id,
name: merchant.shopName, name: merchant.shopName,
coverImage: merchant.logo || '/static/default-avatar.png', coverImage: merchant.logo || DEFAULT_COVER,
cityName: merchant.city, cityName: merchant.city,
rating: merchant.rating, rating: merchant.rating,
reviewCount: merchant.reviewCount, reviewCount: merchant.reviewCount,
@@ -360,27 +244,145 @@ function formatMerchant(merchant: any) {
distance: merchant.distance, distance: merchant.distance,
isRecommend: merchant.isRecommend, isRecommend: merchant.isRecommend,
isHot: merchant.isHot, isHot: merchant.isHot,
isNew: merchant.isNew isNew: merchant.isNew,
}; };
} }
onMounted(() => { // 获取商家列表
// 检查是否有位置搜索回传的关键字 async function fetchMerchantList(shouldReset = false) {
const kw = (uni as any).__locationSearchKeyword; if (isLoading.value) return;
if (kw) { if (!shouldReset && !hasMoreData.value) return;
searchKeyword.value = kw;
(uni as any).__locationSearchKeyword = ''; 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(() => { onActivated(() => {
// 页面激活时也检查(tabBar 切换回来时) checkLocationSearchKeyword();
const kw = (uni as any).__locationSearchKeyword;
if (kw) {
searchKeyword.value = kw;
(uni as any).__locationSearchKeyword = '';
}
}); });
</script> </script>
@@ -398,52 +400,64 @@ onActivated(() => {
/* ========== 搜索面板 ========== */ /* ========== 搜索面板 ========== */
.search-panel { .search-panel {
padding: $spacing-xl; padding: $spacing-xl;
background: linear-gradient(180deg, #f8f9fa 0%, $bg-page 100%);
} }
.search-card { .search-card {
overflow: visible; overflow: visible;
border-radius: $radius-lg;
} }
/* 日期选择器内容 */ /* 日期选择器 */
.date-picker-content { .date-picker-content {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
padding: $spacing-sm 0; padding: $spacing-sm 0;
gap: $spacing-md;
} }
.date-section { .date-section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4rpx; gap: 6rpx;
flex: 1;
&:last-child {
align-items: flex-end;
}
} }
.date-label { .date-label {
font-size: $font-xs; font-size: $font-xs;
color: $text-tertiary; color: $text-tertiary;
line-height: 1.4;
} }
.date-value { .date-value {
font-size: $font-lg; font-size: $font-lg;
font-weight: $font-semibold; font-weight: $font-semibold;
color: $text-primary; color: $text-primary;
line-height: 1.3;
} }
.nights-tag { .nights-badge {
flex-shrink: 0;
font-size: $font-xs; font-size: $font-xs;
color: $primary-color; color: $primary-color;
background: $primary-50; background: linear-gradient(135deg, $primary-50 0%, rgba($primary-color, 0.08) 100%);
padding: 6rpx $spacing-md; padding: 8rpx $spacing-md;
border-radius: $radius-sm; border-radius: $radius-round;
font-weight: $font-medium; font-weight: $font-semibold;
border: 1rpx solid rgba($primary-color, 0.15);
} }
.button-wrapper { .search-button-wrapper {
margin-top: $spacing-md; margin-top: $spacing-lg;
} }
/* ========== 酒店推荐 ========== */ /* ========== 推荐区域 ========== */
.recommend-section { .recommend-section {
flex: 1; flex: 1;
display: flex; display: flex;
@@ -456,31 +470,34 @@ onActivated(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: $spacing-lg 0 $spacing-md; padding: $spacing-xl 0 $spacing-md;
} }
.section-title { .section-title {
font-size: $font-lg; font-size: $font-xl;
font-weight: $font-semibold; font-weight: $font-bold;
color: $text-primary; color: $text-primary;
letter-spacing: 0.5rpx;
} }
.section-more { .section-action {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 4rpx; gap: 6rpx;
font-size: $font-sm; padding: $spacing-xs $spacing-sm;
color: $text-secondary; border-radius: $radius-sm;
transition: $transition-color; transition: $transition-all;
&:active { &:active {
color: $primary-color; background: rgba($primary-color, 0.08);
transform: scale(0.98);
} }
} }
.more-text { .action-text {
font-size: $font-sm; font-size: $font-sm;
color: $text-secondary; color: $text-secondary;
font-weight: $font-medium;
} }
/* ========== 商家列表 ========== */ /* ========== 商家列表 ========== */
@@ -489,52 +506,91 @@ onActivated(() => {
height: 0; height: 0;
} }
.list-content { .merchant-list-content {
padding-bottom: $spacing-xl; padding-bottom: $spacing-2xl;
} }
/* 骨架屏 */ /* 骨架屏 */
.skeleton-merchant { .skeleton-card {
display: flex; display: flex;
gap: $spacing-md; gap: $spacing-md;
padding: $spacing-xl; padding: $spacing-lg;
background: $bg-card; background: $bg-card;
border-radius: $radius-md; border-radius: $radius-lg;
margin-bottom: $spacing-md; margin-bottom: $spacing-md;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
} }
.skeleton-image { .skeleton-image {
width: 200rpx; width: 200rpx;
height: 200rpx; height: 200rpx;
flex-shrink: 0; 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; flex: 1;
display: flex; display: flex;
flex-direction: column; 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 { .skeleton-title {
width: 60%; width: 70%;
height: 36rpx; height: 36rpx;
} }
.skeleton-text { .skeleton-text {
width: 100%; width: 100%;
height: 28rpx;
&.short { &.short {
width: 80%; width: 60%;
} }
} }
/* 没有更多 */ @keyframes skeleton-loading {
.no-more { 0% {
text-align: center; background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* 加载完成提示 */
.load-complete {
display: flex;
align-items: center;
justify-content: center;
padding: $spacing-2xl 0; 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; color: $text-placeholder;
font-size: $font-sm; white-space: nowrap;
} }
</style> </style>
File diff suppressed because it is too large Load Diff
+556 -217
View File
@@ -1,88 +1,196 @@
<template> <template>
<view class="page-order-create"> <view class="page-order-create">
<!-- 房源信息 --> <!-- 房源信息卡片 -->
<view class="room-card" v-if="room"> <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"> <view class="room-info">
<text class="room-name">{{ room.name }}</text> <text class="room-name">{{ room.name }}</text>
<tag-list v-if="roomTags.length" :tags="roomTags" type="primary" size="small" /> <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> </view>
<!-- 入住信息 --> <!-- 入住信息 -->
<view class="section"> <view class="section">
<view class="section-title">入住信息</view> <view class="section-header">
<view class="date-row" @tap="openCalendarPicker"> <view class="section-icon">📅</view>
<view class="date-item"> <text class="section-title">入住信息</text>
<text class="date-label">入住</text> </view>
<text class="date-value">{{ checkInLabel }}</text>
<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>
<view class="date-item"> <view class="date-divider">
<text class="date-label">离店</text> <view class="divider-line"></view>
<text class="date-value">{{ checkOutLabel }}</text> <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>
<view class="night-badge"> <view class="night-badge">
<text class="night-text">{{ nightCount }}</text> <text class="night-text">{{ nightCount }}</text>
</view> </view>
</view> </view>
<!-- 房间数选择 --> <!-- 房间和人数选择 -->
<view class="count-row"> <view class="count-section">
<view class="count-item"> <view class="count-row">
<text class="count-label">房间套数</text> <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"> <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-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> </view>
<text class="count-tip">最多{{ maxRoomCount }}</text>
</view> </view>
</view>
<!-- 入住人数选择 --> <view class="count-divider"></view>
<view class="count-row">
<view class="count-item"> <view class="count-row">
<text class="count-label">入住人数</text> <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"> <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-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> </view>
<text class="count-tip">最多{{ maxGuestCount }}</text>
</view> </view>
</view> </view>
<view class="price-summary"> <!-- 价格明细 -->
<text class="summary-label">房费合计</text> <view class="price-detail">
<price-tag :price="totalPrice" size="large" type="primary" /> <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> </view>
<!-- 联系人信息 --> <!-- 联系人信息 -->
<view class="section"> <view class="section">
<view class="section-title">联系人信息</view> <view class="section-header">
<form-field label="姓名" required> <view class="section-icon">👤</view>
<input class="form-input" v-model="contactName" placeholder="请输入入住人姓名" /> <text class="section-title">联系人信息</text>
</form-field> </view>
<form-field label="手机号" required>
<input class="form-input" v-model="contactPhone" type="number" placeholder="请输入手机号" maxlength="11" /> <view class="form-group">
</form-field> <view class="form-item">
<form-field label="身份证号"> <view class="form-label-row">
<input class="form-input" v-model="contactIdCard" placeholder="请输入身份证号(选填)" maxlength="18" /> <text class="form-label">入住人姓名</text>
</form-field> <text class="form-required">*</text>
<form-field label="备注"> </view>
<textarea class="form-textarea" v-model="remark" placeholder="如有特殊需求请备注" /> <input
</form-field> 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>
<!-- 提交按钮 --> <!-- 温馨提示 -->
<view class="submit-bar"> <view class="tips-section">
<view class="total-row"> <view class="tips-title">📌 温馨提示</view>
<text class="total-label">应付金额</text> <view class="tips-content">
<price-tag :price="totalPrice" size="large" type="primary" /> <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> </view>
<base-button type="primary" size="large" @click="handleSubmit">提交订单</base-button>
</view> </view>
<!-- 日期选择弹窗 --> <!-- 日期选择弹窗 -->
@@ -101,10 +209,8 @@ import { ref, computed, onMounted, watch } from 'vue';
import { getRoomDetail, getRoomCalendar } from '@/api/user/room'; import { getRoomDetail, getRoomCalendar } from '@/api/user/room';
import { createOrder } from '@/api/user/order'; import { createOrder } from '@/api/user/order';
import RoomCalendarPicker from '@/components/RoomCalendarPicker.vue'; import RoomCalendarPicker from '@/components/RoomCalendarPicker.vue';
import BaseButton from '@/components/base/BaseButton.vue';
import PriceTag from '@/components/business/PriceTag.vue'; import PriceTag from '@/components/business/PriceTag.vue';
import TagList from '@/components/business/TagList.vue'; import TagList from '@/components/business/TagList.vue';
import FormField from '@/components/business/FormField.vue';
const roomId = ref(0); const roomId = ref(0);
const merchantId = ref(0); const merchantId = ref(0);
@@ -130,6 +236,8 @@ const typeLabels: Record<string, string> = {
hostel: '青旅', hostel: '青旅',
}; };
const weekdayLabels = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
// 房源标签 // 房源标签
const roomTags = computed(() => { const roomTags = computed(() => {
const tags = []; const tags = [];
@@ -160,6 +268,18 @@ const checkOutLabel = computed(() => {
return `${d.getMonth() + 1}${d.getDate()}`; 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(() => { const maxRoomCount = computed(() => {
if (!checkInDate.value || !checkOutDate.value || calendarStock.value.size === 0) return 1; if (!checkInDate.value || !checkOutDate.value || calendarStock.value.size === 0) return 1;
@@ -316,295 +436,514 @@ function onDateChange(checkIn: string, checkOut: string) {
.page-order-create { .page-order-create {
min-height: 100vh; min-height: 100vh;
background: $bg-page; background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
padding-bottom: 120rpx; padding-bottom: 160rpx;
} }
// 房源卡片
.room-card { .room-card {
display: flex; display: flex;
background: $bg-card; background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
margin: $spacing-xl; margin: 24rpx;
border-radius: $radius-lg; border-radius: 24rpx;
padding: $spacing-xl; padding: 24rpx;
border: 1rpx solid $border-light; 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 { .room-cover {
width: 160rpx; width: 180rpx;
height: 160rpx; height: 180rpx;
border-radius: $radius-base; border-radius: 16rpx;
flex-shrink: 0; 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 { .room-info {
flex: 1; flex: 1;
margin-left: $spacing-lg; margin-left: 24rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
} }
.room-name { .room-name {
font-size: $font-md; font-size: 32rpx;
font-weight: $font-semibold; font-weight: 700;
color: $text-primary; color: $text-primary;
line-height: 1.4;
margin-bottom: 12rpx;
} }
.room-tags { .room-price-row {
display: flex; display: flex;
gap: $spacing-xs; align-items: center;
justify-content: space-between;
} }
.tag { .room-stock-tip {
font-size: $font-xs; font-size: 22rpx;
color: $primary-color; color: #ff4d4f;
background: $primary-bg; background: #fff1f0;
padding: 4rpx $spacing-sm; padding: 4rpx 12rpx;
border-radius: $radius-xs; border-radius: 8rpx;
} font-weight: 500;
.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;
} }
// 通用区块
.section { .section {
background: $bg-card; background: #ffffff;
margin: $spacing-xl; margin: 24rpx;
border-radius: $radius-lg; border-radius: 24rpx;
padding: $spacing-xl; padding: 32rpx;
border: 1rpx solid $border-light; 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 { .section-title {
font-size: $font-base; font-size: 32rpx;
font-weight: $font-semibold; font-weight: 700;
color: $text-primary; 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; display: flex;
align-items: center; align-items: center;
padding: $spacing-md 0; position: relative;
border-bottom: 1rpx solid $border-light; 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; flex: 1;
display: flex; display: flex;
flex-direction: column; 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 { .count-label {
font-size: $font-sm; font-size: 28rpx;
font-weight: 600;
color: $text-primary; color: $text-primary;
flex-shrink: 0; margin-bottom: 4rpx;
width: 140rpx; }
.count-sublabel {
font-size: 22rpx;
color: $text-tertiary;
}
.count-divider {
height: 1rpx;
background: #e8e8e8;
margin: 0 24rpx;
} }
.count-stepper { .count-stepper {
display: flex; display: flex;
align-items: center; 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 { .stepper-btn {
width: 56rpx; width: 64rpx;
height: 56rpx; height: 64rpx;
border-radius: $radius-sm; border-radius: 12rpx;
background: $bg-page; background: linear-gradient(135deg, $primary-color 0%, #ff6b9d 100%);
border: 1rpx solid $border-base;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: $font-lg;
color: $text-primary;
transition: all 0.2s ease; transition: all 0.2s ease;
&:active {
transform: scale(0.9);
}
&.disabled {
background: #f0f0f0;
opacity: 0.5;
}
} }
.stepper-btn:active { .stepper-icon {
background: $bg-hover; font-size: 36rpx;
} font-weight: 700;
color: #ffffff;
.stepper-btn.disabled {
color: $text-disabled;
background: $bg-disabled;
border-color: $border-light;
} }
.stepper-value { .stepper-value {
min-width: 60rpx; min-width: 80rpx;
text-align: center; text-align: center;
font-size: $font-lg; font-size: 32rpx;
font-weight: $font-semibold; font-weight: 700;
color: $text-primary; color: $text-primary;
} }
.count-tip { // 价格明细
font-size: $font-xs; .price-detail {
color: $text-secondary; margin-top: 32rpx;
margin-left: $spacing-md; background: linear-gradient(135deg, #fff7e6 0%, #fff1f0 100%);
border-radius: 20rpx;
padding: 24rpx;
} }
.price-summary { .price-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding-top: $spacing-md; margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
&.total {
margin-top: 16rpx;
padding-top: 16rpx;
}
} }
.summary-label { .price-label {
font-size: $font-sm; font-size: 26rpx;
color: $text-secondary; color: $text-secondary;
} }
.summary-value { .price-value {
font-size: $font-xl; font-size: 28rpx;
font-weight: $font-bold; 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; 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 { .form-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-bottom: $spacing-lg; }
.form-label-row {
display: flex;
align-items: center;
margin-bottom: 16rpx;
} }
.form-label { .form-label {
font-size: $font-sm; font-size: 28rpx;
font-weight: 600;
color: $text-primary; 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 { .form-input {
height: 80rpx; height: 88rpx;
background: $bg-card; background: #f8f9fa;
border: 1rpx solid $border-base; border: 2rpx solid #e8e8e8;
border-radius: $radius-sm; border-radius: 16rpx;
padding: 0 $spacing-md; padding: 0 24rpx;
font-size: $font-base; font-size: 28rpx;
color: $text-primary; color: $text-primary;
transition: all 0.2s ease; transition: all 0.3s ease;
}
.form-input:focus { &:focus {
border-color: $primary-color; background: #ffffff;
border-color: $primary-color;
box-shadow: 0 0 0 4rpx rgba(255, 107, 157, 0.1);
}
} }
.form-textarea { .form-textarea {
height: 120rpx; min-height: 160rpx;
background: $bg-card; background: #f8f9fa;
border: 1rpx solid $border-base; border: 2rpx solid #e8e8e8;
border-radius: $radius-sm; border-radius: 16rpx;
padding: $spacing-md; padding: 20rpx 24rpx;
font-size: $font-base; font-size: 28rpx;
color: $text-primary; 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 { .input-placeholder {
border-color: $primary-color; 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 { .submit-bar {
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: $bg-card; background: #ffffff;
padding: $spacing-xl; padding: 24rpx 24rpx calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid $border-light; border-top: 1rpx solid #f0f0f0;
display: flex; display: flex;
align-items: center; align-items: center;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
z-index: 100;
} }
.total-row { .submit-left {
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; flex-direction: column;
} }
.total-label { .submit-label {
font-size: $font-sm; font-size: 24rpx;
color: $text-secondary; color: $text-secondary;
margin-bottom: 4rpx;
} }
.total-value { .submit-price {
font-size: $font-2xl; display: flex;
font-weight: $font-bold; 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; color: $primary-color;
margin-left: $spacing-xs;
} }
.submit-btn { .submit-btn {
background: $primary-color; background: linear-gradient(135deg, $primary-color 0%, #ff6b9d 100%);
border-radius: $radius-base; border-radius: 48rpx;
height: 88rpx; height: 96rpx;
width: 240rpx; width: 280rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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 { &:active {
background: $primary-dark; transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(255, 107, 157, 0.3);
}
} }
.submit-text { .submit-text {
font-size: $font-lg; font-size: 32rpx;
font-weight: $font-bold; font-weight: 700;
color: #FFFFFF; color: #ffffff;
letter-spacing: 2rpx;
} }
</style> </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),
};
}