dev
This commit is contained in:
@@ -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
@@ -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
@@ -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