feat: 迭代
This commit is contained in:
@@ -7,10 +7,26 @@ export interface ApplyMerchantParams {
|
||||
city?: string;
|
||||
district?: string;
|
||||
address?: string;
|
||||
businessLicense: string;
|
||||
description?: string;
|
||||
coverImage?: string;
|
||||
storeLicense: string;
|
||||
hotelImages?: string;
|
||||
contractType: string;
|
||||
idCardFront?: string;
|
||||
idCardBack?: string;
|
||||
legalIdCardFront?: string;
|
||||
legalIdCardBack?: string;
|
||||
businessLicense?: string;
|
||||
licenseNo?: string;
|
||||
legalPerson?: string;
|
||||
description?: string;
|
||||
accountType: string;
|
||||
accountName?: string;
|
||||
bankAccount: string;
|
||||
bankName: string;
|
||||
bankBranch?: string;
|
||||
bankLicense?: string;
|
||||
accountIdCardFront?: string;
|
||||
accountIdCardBack?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMerchantParams {
|
||||
@@ -18,13 +34,29 @@ export interface UpdateMerchantParams {
|
||||
logo?: string;
|
||||
phone?: string;
|
||||
description?: string;
|
||||
coverImage?: string;
|
||||
province?: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
address?: string;
|
||||
storeLicense?: string;
|
||||
hotelImages?: string;
|
||||
contractType?: string;
|
||||
idCardFront?: string;
|
||||
idCardBack?: string;
|
||||
legalIdCardFront?: string;
|
||||
legalIdCardBack?: string;
|
||||
businessLicense?: string;
|
||||
licenseNo?: string;
|
||||
legalPerson?: string;
|
||||
accountType?: string;
|
||||
accountName?: string;
|
||||
bankAccount?: string;
|
||||
bankName?: string;
|
||||
bankBranch?: string;
|
||||
bankLicense?: string;
|
||||
accountIdCardFront?: string;
|
||||
accountIdCardBack?: string;
|
||||
}
|
||||
|
||||
// 申请创建店铺(需要商家token)
|
||||
|
||||
@@ -1,93 +1,48 @@
|
||||
<template>
|
||||
<view class="merchant-card" @tap="handleClick">
|
||||
<view class="card-container">
|
||||
<!-- 左侧封面图区域 -->
|
||||
<view class="card-cover">
|
||||
<image
|
||||
:src="merchant.coverImage || '/static/default-merchant.png'"
|
||||
class="cover-image"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<view class="card" @tap="handleClick">
|
||||
<view class="card-image">
|
||||
<image
|
||||
:src="merchant.coverImage || '/static/default-merchant.png'"
|
||||
class="img"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-if="merchant.promotion" class="badge-promo">{{ merchant.promotion }}</view>
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="cover-gradient"></view>
|
||||
|
||||
<!-- 视频播放按钮 -->
|
||||
<view v-if="merchant.hasVideo" class="video-badge">
|
||||
<text class="video-icon">▶</text>
|
||||
</view>
|
||||
|
||||
<!-- 类型标签 -->
|
||||
<view v-if="merchant.type" class="type-badge">
|
||||
<text class="type-text">{{ merchant.type }}</text>
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<view class="info-top">
|
||||
<text class="name">{{ merchant.name }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<view class="card-body">
|
||||
<!-- 商家名称 -->
|
||||
<view class="name-row">
|
||||
<text class="merchant-name">{{ merchant.name }}</text>
|
||||
</view>
|
||||
<view v-if="merchant.rating" class="info-rating">
|
||||
<text class="star">★</text>
|
||||
<text class="score">{{ merchant.rating.toFixed(1) }}</text>
|
||||
<text class="reviews">({{ merchant.reviewCount || 0 }})</text>
|
||||
<text v-if="merchant.salesCount" class="dot">·</text>
|
||||
<text v-if="merchant.salesCount" class="sales">已售{{ formatSales(merchant.salesCount) }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 评分和评价 -->
|
||||
<view v-if="merchant.rating" class="rating-row">
|
||||
<view class="stars">
|
||||
<text
|
||||
v-for="star in 5"
|
||||
:key="star"
|
||||
class="star"
|
||||
:class="{ 'star-filled': star <= Math.floor(merchant.rating) }"
|
||||
>
|
||||
{{ star <= Math.floor(merchant.rating) ? '★' : '☆' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="rating-score">{{ merchant.rating.toFixed(1) }}</text>
|
||||
<text class="rating-label">{{ getRatingLabel(merchant.rating) }}</text>
|
||||
<text class="review-count">({{ merchant.reviewCount || 0 }})</text>
|
||||
</view>
|
||||
<view v-if="merchant.city || merchant.district" class="info-location">
|
||||
<text class="location-text">{{ getAddressShort() }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 设施标签 -->
|
||||
<view v-if="merchant.facilities && merchant.facilities.length > 0" class="facilities-row">
|
||||
<view
|
||||
v-for="(facility, index) in merchant.facilities.slice(0, 3)"
|
||||
:key="index"
|
||||
class="facility-item"
|
||||
>
|
||||
<text class="facility-icon">✓</text>
|
||||
<text class="facility-text">{{ facility }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="merchant.facilities && merchant.facilities.length > 0" class="info-tags">
|
||||
<text
|
||||
v-for="(item, i) in merchant.facilities.slice(0, 3)"
|
||||
:key="i"
|
||||
class="tag"
|
||||
>
|
||||
{{ item }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 距离和地标 -->
|
||||
<view v-if="merchant.distance || merchant.nearbyLandmark" class="location-row">
|
||||
<text class="location-icon">📍</text>
|
||||
<text class="location-text">
|
||||
<text v-if="merchant.distance">{{ formatDistance(merchant.distance) }}</text>
|
||||
<text v-if="merchant.distance && merchant.nearbyLandmark"> · </text>
|
||||
<text v-if="merchant.nearbyLandmark">{{ merchant.nearbyLandmark }}</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 购买提示 -->
|
||||
<view v-if="merchant.recentPurchase" class="purchase-row">
|
||||
<text class="purchase-icon">🔥</text>
|
||||
<text class="purchase-text">{{ merchant.recentPurchase }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 价格和按钮 -->
|
||||
<view class="price-row">
|
||||
<view class="price-left">
|
||||
<text class="price-label">低至</text>
|
||||
<text class="price-symbol">¥</text>
|
||||
<text class="price-value">{{ merchant.minPrice }}</text>
|
||||
<text v-if="merchant.originalPrice" class="price-original">¥{{ merchant.originalPrice }}</text>
|
||||
</view>
|
||||
<!-- 促销标签 -->
|
||||
<view v-if="merchant.promotion" class="promotion-badge">
|
||||
<text class="promotion-text">{{ merchant.promotion }}</text>
|
||||
</view>
|
||||
<view class="info-bottom">
|
||||
<view class="price-box">
|
||||
<text class="currency">¥</text>
|
||||
<text class="amount">{{ merchant.minPrice }}</text>
|
||||
<text class="unit">/晚</text>
|
||||
</view>
|
||||
<text v-if="merchant.distance" class="distance">{{ formatDistance(merchant.distance) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -109,6 +64,10 @@ interface Merchant {
|
||||
hasVideo?: boolean
|
||||
recentPurchase?: string
|
||||
promotion?: string
|
||||
city?: string
|
||||
district?: string
|
||||
province?: string
|
||||
salesCount?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -120,138 +79,91 @@ const emit = defineEmits<{
|
||||
(e: 'click', merchant: Merchant): void
|
||||
}>()
|
||||
|
||||
// 获取评分标签
|
||||
const getRatingLabel = (rating: number): string => {
|
||||
if (rating >= 4.5) return '非常棒'
|
||||
if (rating >= 4.0) return '很好'
|
||||
if (rating >= 3.5) return '好'
|
||||
if (rating >= 3.0) return '一般'
|
||||
return '较差'
|
||||
}
|
||||
|
||||
// 格式化距离
|
||||
const formatDistance = (distance: number): string => {
|
||||
if (distance < 1000) {
|
||||
return `${Math.round(distance)}米`
|
||||
return `${Math.round(distance)}m`
|
||||
}
|
||||
return `${(distance / 1000).toFixed(1)}公里`
|
||||
return `${(distance / 1000).toFixed(1)}km`
|
||||
}
|
||||
|
||||
const getAddressShort = (): string => {
|
||||
const parts = []
|
||||
if (props.merchant.city) parts.push(props.merchant.city)
|
||||
if (props.merchant.district) parts.push(props.merchant.district)
|
||||
return parts.join('')
|
||||
}
|
||||
|
||||
const formatSales = (count: number): string => {
|
||||
if (count >= 10000) {
|
||||
return `${(count / 10000).toFixed(1)}万`
|
||||
}
|
||||
if (count >= 1000) {
|
||||
return `${(count / 1000).toFixed(1)}k`
|
||||
}
|
||||
return String(count)
|
||||
}
|
||||
|
||||
// 点击卡片
|
||||
const handleClick = () => {
|
||||
emit('click', props.merchant)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/static/styles/design-tokens.scss';
|
||||
@import '@/static/styles/mixins.scss';
|
||||
|
||||
.merchant-card {
|
||||
.card {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
// 容器:左右布局
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 20rpx;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
// 左侧封面图区域
|
||||
.card-cover {
|
||||
.card-image {
|
||||
position: relative;
|
||||
width: 240rpx;
|
||||
height: 240rpx;
|
||||
width: 260rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
background: #f0f0f0;
|
||||
|
||||
.cover-image {
|
||||
.img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// 底部渐变遮罩
|
||||
.cover-gradient {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80rpx;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.3), transparent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// 视频徽章
|
||||
.video-badge {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(10rpx);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
|
||||
.video-icon {
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 类型标签
|
||||
.type-badge {
|
||||
.badge-promo {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
left: 12rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.4);
|
||||
|
||||
.type-text {
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
background: #e74c3c;
|
||||
border-radius: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧内容区域
|
||||
.card-body {
|
||||
.card-info {
|
||||
flex: 1;
|
||||
padding: 16rpx 16rpx 14rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// 商家名称
|
||||
.name-row {
|
||||
.merchant-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
.info-top {
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
.name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
@@ -262,170 +174,107 @@ const handleClick = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 评分行
|
||||
.rating-row {
|
||||
.info-rating {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
flex-wrap: wrap;
|
||||
gap: 4rpx;
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.stars {
|
||||
display: flex;
|
||||
gap: 2rpx;
|
||||
|
||||
.star {
|
||||
font-size: 24rpx;
|
||||
color: #ddd;
|
||||
line-height: 1;
|
||||
|
||||
&.star-filled {
|
||||
color: #ffa940;
|
||||
}
|
||||
}
|
||||
.star {
|
||||
font-size: 24rpx;
|
||||
color: #fbbf24;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rating-score {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #ffa940;
|
||||
margin-left: 2rpx;
|
||||
.score {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rating-label {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: #ffa940;
|
||||
}
|
||||
|
||||
.review-count {
|
||||
.reviews {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.dot {
|
||||
margin: 0 4rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ddd;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sales {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 设施标签
|
||||
.facilities-row {
|
||||
.info-location {
|
||||
margin-bottom: 10rpx;
|
||||
|
||||
.location-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
.info-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
.facility-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
.tag {
|
||||
padding: 6rpx 12rpx;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #e8ecf1;
|
||||
|
||||
.facility-icon {
|
||||
font-size: 18rpx;
|
||||
color: #52c41a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.facility-text {
|
||||
font-size: 20rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 位置行
|
||||
.location-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
|
||||
.location-icon {
|
||||
font-size: 20rpx;
|
||||
font-size: 22rpx;
|
||||
color: #555;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.location-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
// 购买提示
|
||||
.purchase-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffe8e8 100%);
|
||||
border-radius: 8rpx;
|
||||
border: 1rpx solid #ffccc7;
|
||||
|
||||
.purchase-icon {
|
||||
font-size: 20rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.purchase-text {
|
||||
font-size: 22rpx;
|
||||
color: #ff4d4f;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 价格行
|
||||
.price-row {
|
||||
.info-bottom {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-top: auto;
|
||||
padding-top: 8rpx;
|
||||
|
||||
.price-left {
|
||||
.price-box {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
|
||||
.price-label {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
.currency {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.price-symbol {
|
||||
font-size: 24rpx;
|
||||
.amount {
|
||||
font-size: 42rpx;
|
||||
font-weight: 700;
|
||||
color: #ff4d4f;
|
||||
color: #222;
|
||||
line-height: 1;
|
||||
margin: 0 2rpx;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #ff4d4f;
|
||||
.unit {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.price-original {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
text-decoration: line-through;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 促销标签
|
||||
.promotion-badge {
|
||||
padding: 6rpx 12rpx;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(245, 87, 108, 0.3);
|
||||
|
||||
.promotion-text {
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.distance {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,13 +89,7 @@
|
||||
{
|
||||
"path": "pages/seller/shop-create",
|
||||
"style": {
|
||||
"navigationBarTitleText": "创建店铺"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/seller/shop-edit",
|
||||
"style": {
|
||||
"navigationBarTitleText": "修改店铺"
|
||||
"navigationBarTitleText": "店铺信息"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,71 +1,89 @@
|
||||
<template>
|
||||
<view class="page-index">
|
||||
<!-- 搜索面板 -->
|
||||
<view class="search-panel">
|
||||
<base-card class="search-card" variant="elevated" :shadow="false">
|
||||
<!-- 城市选择 -->
|
||||
<form-field
|
||||
type="select"
|
||||
:display-value="searchParams.city"
|
||||
placeholder="选择目的地城市"
|
||||
right-icon="map"
|
||||
:show-select-arrow="false"
|
||||
@click="openCityPicker"
|
||||
/>
|
||||
|
||||
<!-- 入住离店日期 -->
|
||||
<form-field type="custom" @click="openDatePicker">
|
||||
<view class="date-picker-content">
|
||||
<view class="date-section">
|
||||
<text class="date-label">{{ checkInDesc }}入住</text>
|
||||
<text class="date-value">{{ checkInLabel }}</text>
|
||||
</view>
|
||||
<view class="nights-badge">{{ nightCount }}晚</view>
|
||||
<view class="date-section">
|
||||
<text class="date-label">{{ checkOutDesc }}离店</text>
|
||||
<text class="date-value">{{ checkOutLabel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</form-field>
|
||||
|
||||
<!-- 搜索关键词 -->
|
||||
<form-field
|
||||
type="select"
|
||||
:display-value="searchParams.keyword"
|
||||
placeholder="搜索酒店、民宿、位置"
|
||||
right-icon="search"
|
||||
@click="goLocationSearch"
|
||||
/>
|
||||
|
||||
<!-- 价格区间 -->
|
||||
<form-field
|
||||
type="select"
|
||||
:display-value="priceRangeLabel"
|
||||
placeholder="价格区间"
|
||||
@click="openPricePicker"
|
||||
/>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<view class="search-button-wrapper">
|
||||
<base-button
|
||||
type="primary"
|
||||
size="large"
|
||||
text="搜索酒店"
|
||||
block
|
||||
@click="handleSearch"
|
||||
/>
|
||||
<!-- 头部区域 -->
|
||||
<view class="header-section">
|
||||
<view class="header-top">
|
||||
<view class="city-selector" @tap="openCityPicker">
|
||||
<text class="city-name">{{ searchParams.city }}</text>
|
||||
<u-icon name="arrow-down" :size="16" color="#fff" />
|
||||
</view>
|
||||
</base-card>
|
||||
<view class="header-icon">
|
||||
<u-icon name="bell" :size="22" color="#fff" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="header-title">
|
||||
<text class="title-main">找个心仪的地方</text>
|
||||
<text class="title-sub">开启美好旅程</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐列表 -->
|
||||
<!-- 搜索区域 -->
|
||||
<view class="search-section">
|
||||
<view class="search-box" @tap="goLocationSearch">
|
||||
<u-icon name="search" :size="22" color="#999" />
|
||||
<text class="search-text">{{ searchParams.keyword || '搜索酒店、民宿、位置' }}</text>
|
||||
</view>
|
||||
|
||||
<view class="date-selector" @tap="openDatePicker">
|
||||
<view class="date-info">
|
||||
<view class="date-col">
|
||||
<text class="date-day">{{ checkInLabel }}</text>
|
||||
<text class="date-tip">入住</text>
|
||||
</view>
|
||||
<view class="date-sep">
|
||||
<view class="sep-line" />
|
||||
<text class="sep-nights">{{ nightCount }}晚</text>
|
||||
<view class="sep-line" />
|
||||
</view>
|
||||
<view class="date-col">
|
||||
<text class="date-day">{{ checkOutLabel }}</text>
|
||||
<text class="date-tip">离店</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-row">
|
||||
<view class="filter-btn" @tap="openPricePicker">
|
||||
<u-icon name="rmb-circle" :size="18" color="#666" />
|
||||
<text class="filter-label">{{ priceRangeLabel || '价格筛选' }}</text>
|
||||
</view>
|
||||
<view class="search-btn" @tap="handleSearch">
|
||||
<text class="search-btn-text">搜索</text>
|
||||
<u-icon name="arrow-right" :size="18" color="#fff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类卡片 -->
|
||||
<view class="category-cards">
|
||||
<view
|
||||
v-for="category in categories"
|
||||
:key="category.type"
|
||||
class="category-card"
|
||||
:style="{ background: category.gradient }"
|
||||
@tap="handleCategoryClick(category.type)"
|
||||
>
|
||||
<text class="category-emoji">{{ category.emoji }}</text>
|
||||
<text class="category-title">{{ category.name }}</text>
|
||||
<text class="category-desc">{{ category.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐房源 -->
|
||||
<view class="recommend-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">为你推荐</text>
|
||||
<view class="section-action" @tap="handleSearch">
|
||||
<text class="action-text">查看全部</text>
|
||||
<u-icon name="arrow-right" :size="16" color="#999" />
|
||||
</view>
|
||||
<scroll-view scroll-x class="header-tabs" :show-scrollbar="false">
|
||||
<view
|
||||
v-for="tag in filterTags"
|
||||
:key="tag.value"
|
||||
class="header-tab"
|
||||
:class="{ active: activeTag === tag.value }"
|
||||
@tap="handleTagClick(tag.value)"
|
||||
>
|
||||
{{ tag.label }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
@@ -76,22 +94,20 @@
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
>
|
||||
<!-- 骨架屏加载 -->
|
||||
<loading-state v-if="isLoading && merchants.length === 0" type="skeleton">
|
||||
<template #skeleton>
|
||||
<view v-for="i in 3" :key="i" class="skeleton-card">
|
||||
<view class="skeleton-image" />
|
||||
<view class="skeleton-body">
|
||||
<view class="skeleton-title" />
|
||||
<view class="skeleton-text" />
|
||||
<view class="skeleton-text short" />
|
||||
</view>
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="isLoading && merchants.length === 0" class="skeleton-wrapper">
|
||||
<view v-for="i in 3" :key="i" class="skeleton-card">
|
||||
<view class="skeleton-image" />
|
||||
<view class="skeleton-info">
|
||||
<view class="skeleton-bar long" />
|
||||
<view class="skeleton-bar" />
|
||||
<view class="skeleton-bar short" />
|
||||
</view>
|
||||
</template>
|
||||
</loading-state>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 商家卡片列表 -->
|
||||
<view v-else class="merchant-list-content">
|
||||
<!-- 商家列表 -->
|
||||
<view v-else class="merchant-container">
|
||||
<merchant-card
|
||||
v-for="merchant in displayMerchants"
|
||||
:key="merchant.id"
|
||||
@@ -100,24 +116,28 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多指示器 -->
|
||||
<loading-state v-if="isLoading && merchants.length > 0" size="small" />
|
||||
<!-- 加载中 -->
|
||||
<view v-if="isLoading && merchants.length > 0" class="loading-tip">
|
||||
<loading-state size="small" />
|
||||
</view>
|
||||
|
||||
<!-- 加载完成提示 -->
|
||||
<view v-if="!hasMoreData && merchants.length > 0" class="load-complete">
|
||||
<text class="complete-text">已加载全部内容</text>
|
||||
<!-- 到底了 -->
|
||||
<view v-if="!hasMoreData && merchants.length > 0" class="end-tip">
|
||||
<text class="end-text">· 到底了 ·</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<empty-state
|
||||
v-if="merchants.length === 0 && !isLoading"
|
||||
type="search"
|
||||
title="暂无房源"
|
||||
description="换个条件试试"
|
||||
@action="handleRefresh"
|
||||
/>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 弹窗组件 -->
|
||||
<!-- 弹窗 -->
|
||||
<CityPicker
|
||||
ref="cityPickerRef"
|
||||
:city="searchParams.city"
|
||||
@@ -148,24 +168,63 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onActivated, reactive } from 'vue';
|
||||
import { getMerchantList } from '@/api/user/merchant';
|
||||
import { getDefaultDates, formatDateShort, getDateDescription, calculateNights } from '@/utils/date';
|
||||
import { getDefaultDates, formatDateShort, calculateNights } from '@/utils/date';
|
||||
import type { Merchant, MerchantCardData, SearchParams } from '@/types/merchant';
|
||||
import CityPicker from '@/components/CityPicker.vue';
|
||||
import DatePicker from '@/components/DatePicker.vue';
|
||||
import RoomGuestPicker from '@/components/RoomGuestPicker.vue';
|
||||
import PricePicker from '@/components/PricePicker.vue';
|
||||
import BaseCard from '@/components/base/BaseCard.vue';
|
||||
import BaseButton from '@/components/base/BaseButton.vue';
|
||||
import FormField from '@/components/business/FormField.vue';
|
||||
import MerchantCard from '@/components/business/MerchantCard.vue';
|
||||
import LoadingState from '@/components/base/LoadingState.vue';
|
||||
import EmptyState from '@/components/base/EmptyState.vue';
|
||||
|
||||
// 常量定义
|
||||
// 常量
|
||||
const DEFAULT_CITY = '上海';
|
||||
const PAGE_SIZE = 10;
|
||||
const DEFAULT_COVER = '/static/default-avatar.png';
|
||||
|
||||
// 分类数据
|
||||
const categories = ref([
|
||||
{
|
||||
type: 'hotel',
|
||||
name: '精品酒店',
|
||||
emoji: '🏨',
|
||||
desc: '舒适商务之选',
|
||||
gradient: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
},
|
||||
{
|
||||
type: 'homestay',
|
||||
name: '特色民宿',
|
||||
emoji: '🏡',
|
||||
desc: '体验当地生活',
|
||||
gradient: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
|
||||
},
|
||||
{
|
||||
type: 'apartment',
|
||||
name: '品质公寓',
|
||||
emoji: '🏢',
|
||||
desc: '长住更优惠',
|
||||
gradient: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
|
||||
},
|
||||
{
|
||||
type: 'hostel',
|
||||
name: '青年旅舍',
|
||||
emoji: '🎒',
|
||||
desc: '结识新朋友',
|
||||
gradient: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
|
||||
},
|
||||
]);
|
||||
|
||||
// 筛选标签
|
||||
const filterTags = ref([
|
||||
{ label: '推荐', value: 'recommend' },
|
||||
{ label: '特价', value: 'discount' },
|
||||
{ label: '高分', value: 'rating' },
|
||||
{ label: '新房源', value: 'new' },
|
||||
]);
|
||||
|
||||
const activeTag = ref('recommend');
|
||||
|
||||
// 获取默认日期
|
||||
const { today, tomorrow } = getDefaultDates();
|
||||
|
||||
@@ -208,14 +267,6 @@ 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 '';
|
||||
@@ -233,8 +284,11 @@ function transformMerchantData(merchant: Merchant): MerchantCardData {
|
||||
return {
|
||||
id: merchant.id,
|
||||
name: merchant.shopName,
|
||||
coverImage: merchant.logo || DEFAULT_COVER,
|
||||
coverImage: merchant.coverImage || merchant.logo || DEFAULT_COVER,
|
||||
cityName: merchant.city,
|
||||
city: merchant.city,
|
||||
district: merchant.district,
|
||||
province: merchant.province,
|
||||
rating: merchant.rating,
|
||||
reviewCount: merchant.reviewCount,
|
||||
description: merchant.description || '暂无简介',
|
||||
@@ -245,6 +299,7 @@ function transformMerchantData(merchant: Merchant): MerchantCardData {
|
||||
isRecommend: merchant.isRecommend,
|
||||
isHot: merchant.isHot,
|
||||
isNew: merchant.isNew,
|
||||
salesCount: merchant.salesCount,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -328,6 +383,18 @@ function handleMerchantClick(merchantId: number) {
|
||||
uni.navigateTo({ url: `/pages/merchant-detail/index?${queryParams}` });
|
||||
}
|
||||
|
||||
// 分类点击
|
||||
function handleCategoryClick(type: string) {
|
||||
const queryParams = buildSearchQuery();
|
||||
uni.navigateTo({ url: `/pages/search/index?${queryParams}&type=${type}` });
|
||||
}
|
||||
|
||||
// 标签点击
|
||||
function handleTagClick(tag: string) {
|
||||
activeTag.value = tag;
|
||||
fetchMerchantList(true);
|
||||
}
|
||||
|
||||
// 构建查询参数
|
||||
function buildSearchQuery(): string {
|
||||
const params = [
|
||||
@@ -392,112 +459,323 @@ onActivated(() => {
|
||||
|
||||
.page-index {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
background: #f8f9fb;
|
||||
}
|
||||
|
||||
/* ========== 头部区域 ========== */
|
||||
.header-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 24rpx 32rpx 80rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* ========== 搜索面板 ========== */
|
||||
.search-panel {
|
||||
padding: $spacing-xl;
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, $bg-page 100%);
|
||||
}
|
||||
|
||||
.search-card {
|
||||
overflow: visible;
|
||||
border-radius: $radius-lg;
|
||||
}
|
||||
|
||||
/* 日期选择器 */
|
||||
.date-picker-content {
|
||||
.city-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: $spacing-sm 0;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50rpx;
|
||||
backdrop-filter: blur(10rpx);
|
||||
transition: all 0.3s;
|
||||
|
||||
.date-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
flex: 1;
|
||||
|
||||
&:last-child {
|
||||
align-items: flex-end;
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: $font-xs;
|
||||
color: $text-tertiary;
|
||||
line-height: 1.4;
|
||||
.city-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.date-value {
|
||||
font-size: $font-lg;
|
||||
font-weight: $font-semibold;
|
||||
color: $text-primary;
|
||||
line-height: 1.3;
|
||||
.header-icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(10rpx);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.nights-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: $font-xs;
|
||||
color: $primary-color;
|
||||
background: linear-gradient(135deg, $primary-50 0%, rgba($primary-color, 0.08) 100%);
|
||||
padding: 8rpx $spacing-md;
|
||||
border-radius: $radius-round;
|
||||
font-weight: $font-semibold;
|
||||
border: 1rpx solid rgba($primary-color, 0.15);
|
||||
.header-title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.search-button-wrapper {
|
||||
margin-top: $spacing-lg;
|
||||
.title-main {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
/* ========== 推荐区域 ========== */
|
||||
.recommend-section {
|
||||
.title-sub {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* ========== 搜索区域 ========== */
|
||||
.search-section {
|
||||
margin: -60rpx 32rpx 32rpx;
|
||||
padding: 32rpx;
|
||||
background: #fff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(102, 126, 234, 0.15);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: #ebeef5;
|
||||
}
|
||||
}
|
||||
|
||||
.search-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.date-selector {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
|
||||
border-radius: 16rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: linear-gradient(135deg, #ebeef5 0%, #dfe3e8 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.date-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.date-col {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 $spacing-xl;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $spacing-xl 0 $spacing-md;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: $font-xl;
|
||||
font-weight: $font-bold;
|
||||
color: $text-primary;
|
||||
letter-spacing: 0.5rpx;
|
||||
.date-day {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.section-action {
|
||||
.date-tip {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.date-sep {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.sep-line {
|
||||
width: 40rpx;
|
||||
height: 2rpx;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.sep-nights {
|
||||
font-size: 20rpx;
|
||||
color: $primary-color;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
background: rgba($primary-color, 0.1);
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
transition: $transition-all;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 12rpx;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
background: rgba($primary-color, 0.08);
|
||||
background: #ebeef5;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: $font-sm;
|
||||
color: $text-secondary;
|
||||
font-weight: $font-medium;
|
||||
.filter-label {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
flex: 1.5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(102, 126, 234, 0.4);
|
||||
transition: all 0.3s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 2rpx 8rpx rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ========== 分类卡片 ========== */
|
||||
.category-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16rpx;
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.category-card {
|
||||
padding: 32rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 0 0 0 100rpx;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(-4rpx);
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.category-emoji {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.category-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.category-desc {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* ========== 推荐房源 ========== */
|
||||
.recommend-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-tab {
|
||||
display: inline-block;
|
||||
padding: 12rpx 28rpx;
|
||||
margin-right: 16rpx;
|
||||
background: #f5f7fa;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
box-shadow: 0 4rpx 12rpx rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 商家列表 ========== */
|
||||
@@ -506,58 +784,59 @@ onActivated(() => {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.merchant-list-content {
|
||||
padding-bottom: $spacing-2xl;
|
||||
.merchant-container {
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
/* 骨架屏 */
|
||||
.skeleton-wrapper {
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
padding: $spacing-lg;
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
gap: 24rpx;
|
||||
padding: 24rpx;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.skeleton-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: $radius-md;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-body {
|
||||
.skeleton-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $spacing-md;
|
||||
justify-content: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.skeleton-title,
|
||||
.skeleton-text {
|
||||
height: 32rpx;
|
||||
border-radius: $radius-sm;
|
||||
.skeleton-bar {
|
||||
height: 28rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 70%;
|
||||
height: 36rpx;
|
||||
}
|
||||
|
||||
.skeleton-text {
|
||||
width: 100%;
|
||||
&.long {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
&.short {
|
||||
width: 60%;
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,27 +849,22 @@ onActivated(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载完成提示 */
|
||||
.load-complete {
|
||||
/* 加载提示 */
|
||||
.loading-tip {
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-2xl 0;
|
||||
position: relative;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1rpx;
|
||||
background: linear-gradient(to right, transparent, $border-light, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.complete-text {
|
||||
padding: 0 $spacing-xl;
|
||||
font-size: $font-xs;
|
||||
color: $text-placeholder;
|
||||
white-space: nowrap;
|
||||
.end-tip {
|
||||
padding: 48rpx 32rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.end-text {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -339,8 +339,26 @@ const roomGuestPickerRef = ref();
|
||||
|
||||
// 计算属性
|
||||
const merchantImages = computed(() => {
|
||||
const coverImage = merchantData.value.coverImage;
|
||||
const images = merchantData.value.images || [];
|
||||
return images.length > 0 ? images : [merchantData.value.logo || DEFAULT_AVATAR];
|
||||
|
||||
// 第一张使用封面图,后面使用店铺图片
|
||||
const result: string[] = [];
|
||||
|
||||
if (coverImage) {
|
||||
result.push(coverImage);
|
||||
}
|
||||
|
||||
if (images.length > 0) {
|
||||
result.push(...images);
|
||||
}
|
||||
|
||||
// 如果没有任何图片,使用默认头像
|
||||
if (result.length === 0) {
|
||||
result.push(DEFAULT_AVATAR);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
const fullAddress = computed(() => {
|
||||
@@ -599,6 +617,7 @@ onMounted(() => {
|
||||
.page-merchant-detail {
|
||||
min-height: 100vh;
|
||||
background: #F5F7FA;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
/* ========== 顶部轮播图 ========== */
|
||||
@@ -641,6 +660,8 @@ onMounted(() => {
|
||||
|
||||
/* ========== 商家信息卡片 ========== */
|
||||
.merchant-info-card {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin: -60rpx 24rpx 24rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 24rpx;
|
||||
|
||||
@@ -1,72 +1,75 @@
|
||||
<template>
|
||||
<view class="page-search">
|
||||
<!-- 头部搜索栏 -->
|
||||
<view class="search-page">
|
||||
<!-- 搜索头部 -->
|
||||
<view class="search-header">
|
||||
<view class="header-top">
|
||||
<view class="back-btn" @tap="goBack">
|
||||
<u-icon name="arrow-left" size="20" color="#333"></u-icon>
|
||||
<view class="search-bar">
|
||||
<view class="back-icon" @tap="goBack">
|
||||
<u-icon name="arrow-left" size="20" color="#333" />
|
||||
</view>
|
||||
<view class="search-box">
|
||||
<u-icon name="search" size="18" color="#999"></u-icon>
|
||||
<view class="input-wrapper">
|
||||
<u-icon name="search" size="18" color="#999" />
|
||||
<input
|
||||
v-model="keyword"
|
||||
type="text"
|
||||
placeholder="搜索酒店/品牌"
|
||||
placeholder="搜索酒店、民宿、品牌"
|
||||
confirm-type="search"
|
||||
class="search-input"
|
||||
class="input"
|
||||
@confirm="handleSearch"
|
||||
/>
|
||||
<u-icon v-if="keyword" name="close-circle-fill" size="18" color="#ccc" @click="clearKeyword"></u-icon>
|
||||
<view v-if="keyword" class="clear-icon" @tap="clearKeyword">
|
||||
<u-icon name="close-circle-fill" size="16" color="#ccc" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="search-btn" @tap="handleSearch">搜索</text>
|
||||
<text class="search-text" @tap="handleSearch">搜索</text>
|
||||
</view>
|
||||
|
||||
<view class="filter-info">
|
||||
<view class="info-item">
|
||||
<u-icon name="map-fill" size="14" color="#ff6b35"></u-icon>
|
||||
<text class="info-text">{{ city }}</text>
|
||||
<view class="filter-bar">
|
||||
<view class="filter-item">
|
||||
<u-icon name="map-fill" size="14" color="#ff6b35" />
|
||||
<text class="filter-text">{{ city }}</text>
|
||||
</view>
|
||||
<view class="info-divider"></view>
|
||||
<view class="info-item">
|
||||
<u-icon name="calendar-fill" size="14" color="#ff6b35"></u-icon>
|
||||
<text class="info-text">{{ checkInLabel }}-{{ checkOutLabel }} · {{ nightCount }}晚</text>
|
||||
<view class="divider" />
|
||||
<view class="filter-item">
|
||||
<u-icon name="calendar-fill" size="14" color="#ff6b35" />
|
||||
<text class="filter-text">{{ dateRangeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 结果统计 -->
|
||||
<view v-if="!loading && merchantList.length > 0" class="result-count">
|
||||
<text class="count-text">为您找到 <text class="count-num">{{ merchantList.length }}+</text> 家酒店</text>
|
||||
<view v-if="!loading && merchantList.length > 0" class="result-info">
|
||||
<text class="result-text">找到 <text class="result-num">{{ merchantList.length }}</text> 家</text>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果列表 -->
|
||||
<!-- 列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="result-list"
|
||||
class="scroll-view"
|
||||
@scrolltolower="loadMore"
|
||||
refresher-enabled
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<view class="list-content">
|
||||
<view class="list-wrapper">
|
||||
<MerchantCard
|
||||
v-for="merchant in merchantList"
|
||||
:key="merchant.id"
|
||||
:merchant="formatMerchant(merchant)"
|
||||
@click="goMerchant(merchant.id)"
|
||||
class="merchant-item"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<LoadingState v-if="loading" mode="dots" />
|
||||
|
||||
<view v-if="!hasMore && merchantList.length > 0" class="no-more">
|
||||
<text class="no-more-text">— 已经到底了 —</text>
|
||||
<view v-if="!hasMore && merchantList.length > 0" class="load-end">
|
||||
<text class="end-text">没有更多了</text>
|
||||
</view>
|
||||
|
||||
<EmptyState
|
||||
v-if="merchantList.length === 0 && !loading"
|
||||
type="search"
|
||||
description="试试调整搜索条件"
|
||||
description="换个关键词试试"
|
||||
/>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@@ -79,7 +82,6 @@ import MerchantCard from '@/components/business/MerchantCard.vue';
|
||||
import EmptyState from '@/components/base/EmptyState.vue';
|
||||
import LoadingState from '@/components/base/LoadingState.vue';
|
||||
|
||||
// 筛选条件(从URL获取)
|
||||
const city = ref('上海');
|
||||
const keyword = ref('');
|
||||
const checkInDate = ref('');
|
||||
@@ -90,35 +92,24 @@ const childCount = ref(0);
|
||||
const minPrice = ref<number | undefined>(undefined);
|
||||
const maxPrice = ref<number | undefined>(undefined);
|
||||
|
||||
// 商家列表
|
||||
const merchantList = ref<any[]>([]);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const hasMore = ref(true);
|
||||
|
||||
const nightCount = computed(() => {
|
||||
if (!checkInDate.value || !checkOutDate.value) return 1;
|
||||
const diff = new Date(checkOutDate.value).getTime() - new Date(checkInDate.value).getTime();
|
||||
return Math.max(1, Math.ceil(diff / (1000 * 60 * 60 * 24)));
|
||||
});
|
||||
|
||||
const checkInLabel = computed(() => {
|
||||
if (!checkInDate.value) return '';
|
||||
const d = new Date(checkInDate.value);
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日`;
|
||||
});
|
||||
|
||||
const checkOutLabel = computed(() => {
|
||||
if (!checkOutDate.value) return '';
|
||||
const d = new Date(checkOutDate.value);
|
||||
return `${d.getMonth() + 1}月${d.getDate()}日`;
|
||||
const dateRangeText = computed(() => {
|
||||
if (!checkInDate.value || !checkOutDate.value) return '选择日期';
|
||||
const checkIn = new Date(checkInDate.value);
|
||||
const checkOut = new Date(checkOutDate.value);
|
||||
const nights = Math.max(1, Math.ceil((checkOut.getTime() - checkIn.getTime()) / (1000 * 60 * 60 * 24)));
|
||||
return `${checkIn.getMonth() + 1}/${checkIn.getDate()}-${checkOut.getMonth() + 1}/${checkOut.getDate()} · ${nights}晚`;
|
||||
});
|
||||
|
||||
function initFromUrl() {
|
||||
const pages = getCurrentPages();
|
||||
const page = pages[pages.length - 1] as any;
|
||||
const options = page.options || page.$page?.options || {};
|
||||
const currentPage = pages[pages.length - 1] as any;
|
||||
const options = currentPage.options || currentPage.$page?.options || {};
|
||||
|
||||
city.value = options.city ? decodeURIComponent(options.city) : '上海';
|
||||
keyword.value = options.keyword ? decodeURIComponent(options.keyword) : '';
|
||||
@@ -198,335 +189,158 @@ function clearKeyword() {
|
||||
keyword.value = '';
|
||||
}
|
||||
|
||||
// 格式化商家数据以适配 MerchantCard 组件
|
||||
function formatMerchant(merchant: any) {
|
||||
return {
|
||||
id: merchant.id,
|
||||
name: merchant.shopName,
|
||||
coverImage: merchant.logo || '/static/default-avatar.png',
|
||||
cityName: merchant.city,
|
||||
coverImage: merchant.coverImage || merchant.logo || '/static/default-avatar.png',
|
||||
city: merchant.city,
|
||||
district: merchant.district,
|
||||
province: merchant.province,
|
||||
rating: merchant.rating,
|
||||
reviewCount: merchant.reviewCount,
|
||||
description: merchant.description || '舒适住宿,温馨服务',
|
||||
facilities: merchant.facilities || [],
|
||||
minPrice: merchant.minPrice || 0,
|
||||
roomCount: merchant.roomCount,
|
||||
originalPrice: merchant.originalPrice,
|
||||
distance: merchant.distance,
|
||||
isRecommend: merchant.isRecommend,
|
||||
isHot: merchant.isHot,
|
||||
isNew: merchant.isNew
|
||||
salesCount: merchant.salesCount,
|
||||
promotion: merchant.promotion,
|
||||
type: merchant.type,
|
||||
hasVideo: merchant.hasVideo,
|
||||
recentPurchase: merchant.recentPurchase,
|
||||
nearbyLandmark: merchant.nearbyLandmark,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/static/styles/common.scss';
|
||||
|
||||
.page-search {
|
||||
.search-page {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 搜索头部 - 扁平化设计 */
|
||||
// 搜索头部
|
||||
.search-header {
|
||||
background: $bg-card;
|
||||
padding: $spacing-lg $spacing-2xl $spacing-xl;
|
||||
border-bottom: 1rpx solid $border-light;
|
||||
background: #fff;
|
||||
padding: 16rpx 24rpx 20rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.header-top {
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-md;
|
||||
margin-bottom: $spacing-lg;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
.back-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-base;
|
||||
border: 1rpx solid $border-light;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
background: $bg-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
.input-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $bg-page;
|
||||
border: 1rpx solid $border-light;
|
||||
border-radius: $radius-base;
|
||||
gap: 12rpx;
|
||||
height: 72rpx;
|
||||
padding: 0 $spacing-xl;
|
||||
gap: $spacing-sm;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0 20rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 36rpx;
|
||||
min-width: 0;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $primary-color;
|
||||
background: $bg-card;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
.input {
|
||||
flex: 1;
|
||||
font-size: $font-base;
|
||||
color: $text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.search-btn {
|
||||
font-size: $font-base;
|
||||
color: $primary-color;
|
||||
font-weight: $font-semibold;
|
||||
padding: 0 $spacing-xs;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-lg;
|
||||
padding: $spacing-md 0 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
|
||||
.info-text {
|
||||
font-size: $font-sm;
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.info-divider {
|
||||
width: 1rpx;
|
||||
height: $spacing-xl;
|
||||
background: $border-base;
|
||||
}
|
||||
|
||||
/* 结果统计 */
|
||||
.result-count {
|
||||
padding: $spacing-xl $spacing-2xl $spacing-md;
|
||||
background: $bg-page;
|
||||
|
||||
.count-text {
|
||||
font-size: $font-sm;
|
||||
color: $text-secondary;
|
||||
|
||||
.count-num {
|
||||
color: $primary-color;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 列表区域 */
|
||||
.result-list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
padding: 0 $spacing-2xl $spacing-2xl;
|
||||
}
|
||||
|
||||
/* 酒店卡片 - 扁平化设计 */
|
||||
.hotel-card {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
border: 1rpx solid $border-light;
|
||||
overflow: hidden;
|
||||
margin-top: $spacing-xl;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
background: $bg-hover;
|
||||
border-color: $border-base;
|
||||
}
|
||||
}
|
||||
|
||||
.card-image-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
overflow: hidden;
|
||||
|
||||
.card-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
height: 72rpx;
|
||||
line-height: 72rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rating-badge {
|
||||
position: absolute;
|
||||
top: $spacing-lg;
|
||||
right: $spacing-lg;
|
||||
background: $bg-card;
|
||||
border: 1rpx solid $border-light;
|
||||
padding: $spacing-xs $spacing-md;
|
||||
border-radius: $radius-base;
|
||||
.clear-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.rating-score {
|
||||
font-size: $font-sm;
|
||||
color: $text-primary;
|
||||
font-weight: $font-semibold;
|
||||
.search-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ff6b35;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
|
||||
.filter-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 1rpx;
|
||||
height: 24rpx;
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
// 结果统计
|
||||
.result-info {
|
||||
padding: 20rpx 24rpx 12rpx;
|
||||
background: #f5f5f5;
|
||||
|
||||
.result-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
|
||||
.result-num {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: $spacing-xl;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.hotel-name {
|
||||
// 列表
|
||||
.scroll-view {
|
||||
flex: 1;
|
||||
font-size: $font-lg;
|
||||
font-weight: $font-semibold;
|
||||
color: $text-primary;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hotel-tags {
|
||||
display: flex;
|
||||
gap: $spacing-xs;
|
||||
flex-shrink: 0;
|
||||
.list-wrapper {
|
||||
padding: 12rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
font-size: $font-xs;
|
||||
color: $text-secondary;
|
||||
background: $bg-page;
|
||||
border: 1rpx solid $border-light;
|
||||
padding: 6rpx $spacing-sm;
|
||||
border-radius: $radius-sm;
|
||||
.merchant-item {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.hotel-desc {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
line-height: 1.6;
|
||||
margin-bottom: $spacing-lg;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-top: $spacing-lg;
|
||||
border-top: 1rpx solid $border-light;
|
||||
}
|
||||
|
||||
.price-section {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
|
||||
.price-symbol {
|
||||
font-size: $font-base;
|
||||
color: $primary-color;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.price-amount {
|
||||
font-size: 44rpx;
|
||||
font-weight: $font-bold;
|
||||
color: $primary-color;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.price-suffix {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
.book-btn {
|
||||
background: $primary-color;
|
||||
padding: $spacing-md $spacing-2xl;
|
||||
border-radius: $radius-base;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:active {
|
||||
background: $primary-dark;
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: $font-sm;
|
||||
color: #fff;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-more {
|
||||
.load-end {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
padding: $spacing-3xl 0;
|
||||
|
||||
.loading-text {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
}
|
||||
|
||||
.no-more {
|
||||
text-align: center;
|
||||
padding: $spacing-3xl 0;
|
||||
|
||||
.no-more-text {
|
||||
font-size: $font-sm;
|
||||
color: $text-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 160rpx 0;
|
||||
gap: $spacing-xl;
|
||||
|
||||
.empty-title {
|
||||
font-size: $font-lg;
|
||||
color: $text-secondary;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: $font-sm;
|
||||
color: $text-tertiary;
|
||||
.end-text {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<view class="header-bg"></view>
|
||||
<view class="header-content">
|
||||
<view class="shop-main">
|
||||
<image class="shop-avatar" :src="merchant.logo || '/static/default-avatar.png'" mode="aspectFill" />
|
||||
<image class="shop-avatar" :src="merchant.coverImage || merchant.logo || '/static/default-avatar.png'" mode="aspectFill" />
|
||||
<view class="shop-info">
|
||||
<text class="shop-name">{{ merchant.shopName }}</text>
|
||||
<view class="shop-meta">
|
||||
@@ -151,6 +151,43 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单(仅审核通过显示) -->
|
||||
<view v-if="merchant.status === 'approved'" class="menu-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">快捷功能</text>
|
||||
</view>
|
||||
<view class="menu-grid">
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/orders')">
|
||||
<view class="menu-icon-wrapper primary">
|
||||
<u-icon name="list" size="32" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">订单管理</text>
|
||||
<text class="menu-desc">查看和处理订单</text>
|
||||
</view>
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/rooms')">
|
||||
<view class="menu-icon-wrapper success">
|
||||
<u-icon name="home" size="40" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">房源管理</text>
|
||||
<text class="menu-desc">管理房源信息</text>
|
||||
</view>
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/room-calendar')">
|
||||
<view class="menu-icon-wrapper warning">
|
||||
<u-icon name="calendar" size="32" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">房量房价</text>
|
||||
<text class="menu-desc">设置价格日历</text>
|
||||
</view>
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/shop-create?mode=edit')">
|
||||
<view class="menu-icon-wrapper info">
|
||||
<u-icon name="setting" size="32" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">店铺设置</text>
|
||||
<text class="menu-desc">修改店铺信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺信息 -->
|
||||
<view v-if="merchant.status !== 'pending'" class="info-section">
|
||||
<view class="section-header">
|
||||
@@ -186,43 +223,6 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单(仅审核通过显示) -->
|
||||
<view v-if="merchant.status === 'approved'" class="menu-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">快捷功能</text>
|
||||
</view>
|
||||
<view class="menu-grid">
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/orders')">
|
||||
<view class="menu-icon-wrapper primary">
|
||||
<u-icon name="list" size="32" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">订单管理</text>
|
||||
<text class="menu-desc">查看和处理订单</text>
|
||||
</view>
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/rooms')">
|
||||
<view class="menu-icon-wrapper success">
|
||||
<u-icon name="home" size="40" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">房源管理</text>
|
||||
<text class="menu-desc">管理房源信息</text>
|
||||
</view>
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/room-calendar')">
|
||||
<view class="menu-icon-wrapper warning">
|
||||
<u-icon name="calendar" size="32" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">房量房价</text>
|
||||
<text class="menu-desc">设置价格日历</text>
|
||||
</view>
|
||||
<view class="menu-card" @tap="navigateTo('/pages/seller/settings')">
|
||||
<view class="menu-icon-wrapper info">
|
||||
<u-icon name="setting" size="32" color="#333"></u-icon>
|
||||
</view>
|
||||
<text class="menu-label">店铺设置</text>
|
||||
<text class="menu-desc">修改店铺信息</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -274,7 +274,7 @@ function goCreateShop() {
|
||||
}
|
||||
|
||||
function goEditShop() {
|
||||
uni.navigateTo({ url: '/pages/seller/shop-edit' });
|
||||
uni.navigateTo({ url: '/pages/seller/shop-create?mode=edit' });
|
||||
}
|
||||
|
||||
function handleLogoutSeller() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,94 +1,165 @@
|
||||
<template>
|
||||
<view class="page-room-form">
|
||||
<view class="form-section">
|
||||
<!-- 房源名称 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">房源名称</text>
|
||||
<input class="input" type="text" v-model="form.name" placeholder="请输入房源名称" maxlength="100" />
|
||||
<!-- 基本信息 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">基本信息</text>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<view class="field-label">
|
||||
<text class="label-text">房源名称</text>
|
||||
<text class="label-required">*</text>
|
||||
</view>
|
||||
<input
|
||||
class="field-input"
|
||||
type="text"
|
||||
v-model="form.name"
|
||||
placeholder="如:豪华海景大床房"
|
||||
maxlength="100"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 类型 + 价格 -->
|
||||
<view class="form-row">
|
||||
<view class="form-item half">
|
||||
<text class="label required">类型</text>
|
||||
<view class="form-group half">
|
||||
<view class="field-label">
|
||||
<text class="label-text">房型</text>
|
||||
<text class="label-required">*</text>
|
||||
</view>
|
||||
<picker :range="typeOptions" range-key="label" :value="typeIndex" @change="onTypeChange">
|
||||
<view class="picker-value">{{ typeOptions[typeIndex].label }}</view>
|
||||
<view class="field-picker">
|
||||
<text class="picker-text">{{ typeOptions[typeIndex].label }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item half">
|
||||
<text class="label required">价格/晚</text>
|
||||
<input class="input" type="digit" v-model="form.price" placeholder="0.00" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 床型 -->
|
||||
<view class="form-item">
|
||||
<text class="label">床型</text>
|
||||
<picker :range="bedTypeOptions" range-key="label" :value="bedTypeIndex" @change="onBedTypeChange">
|
||||
<view class="picker-value">{{ bedTypeOptions[bedTypeIndex].label }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 面积 -->
|
||||
<view class="form-item">
|
||||
<text class="label">面积(m²)</text>
|
||||
<input class="input" type="digit" v-model="form.area" placeholder="0" />
|
||||
</view>
|
||||
|
||||
<!-- 最多入住 -->
|
||||
<view class="form-item">
|
||||
<text class="label">最多入住</text>
|
||||
<input class="input" type="number" v-model="form.maxGuests" placeholder="1" />
|
||||
</view>
|
||||
|
||||
<!-- 取消政策 -->
|
||||
<view class="form-item">
|
||||
<text class="label">取消政策</text>
|
||||
<picker :range="policyOptions" range-key="label" :value="policyIndex" @change="onPolicyChange">
|
||||
<view class="picker-value">{{ policyOptions[policyIndex].label }}</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 图片上传 -->
|
||||
<view class="form-item">
|
||||
<text class="label">房源图片</text>
|
||||
<view class="image-list">
|
||||
<view v-for="(img, i) in imageList" :key="i" class="image-item">
|
||||
<image class="image-preview" :src="img" mode="aspectFill" />
|
||||
<view class="image-delete" @tap="removeImage(i)">×</view>
|
||||
<view class="form-group half">
|
||||
<view class="field-label">
|
||||
<text class="label-text">价格</text>
|
||||
<text class="label-required">*</text>
|
||||
</view>
|
||||
<view v-if="imageList.length < 20" class="image-add" @tap="chooseImage">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="hint">第一张为封面,最多20张</text>
|
||||
</view>
|
||||
|
||||
<!-- 设施选择 -->
|
||||
<view class="form-item">
|
||||
<text class="label">设施服务</text>
|
||||
<view class="facility-list">
|
||||
<view
|
||||
v-for="f in facilityOptions"
|
||||
:key="f"
|
||||
:class="['facility-tag', { active: form.facilities.includes(f) }]"
|
||||
@tap="toggleFacility(f)"
|
||||
>
|
||||
{{ f }}
|
||||
<view class="field-input-wrapper">
|
||||
<text class="input-prefix">¥</text>
|
||||
<input class="field-input with-prefix" type="digit" v-model="form.price" placeholder="0" />
|
||||
<text class="input-suffix">/晚</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 描述 -->
|
||||
<view class="form-item">
|
||||
<text class="label">房源描述</text>
|
||||
<textarea class="textarea" v-model="form.description" placeholder="请描述房源特色" maxlength="1000" />
|
||||
<view class="form-row">
|
||||
<view class="form-group half">
|
||||
<view class="field-label">
|
||||
<text class="label-text">床型</text>
|
||||
</view>
|
||||
<picker :range="bedTypeOptions" range-key="label" :value="bedTypeIndex" @change="onBedTypeChange">
|
||||
<view class="field-picker">
|
||||
<text class="picker-text">{{ bedTypeOptions[bedTypeIndex].label }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-group half">
|
||||
<view class="field-label">
|
||||
<text class="label-text">面积</text>
|
||||
</view>
|
||||
<view class="field-input-wrapper">
|
||||
<input class="field-input" type="digit" v-model="form.area" placeholder="0" />
|
||||
<text class="input-suffix">m²</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-group half">
|
||||
<view class="field-label">
|
||||
<text class="label-text">最多入住</text>
|
||||
</view>
|
||||
<view class="field-input-wrapper">
|
||||
<input class="field-input" type="number" v-model="form.maxGuests" placeholder="1" />
|
||||
<text class="input-suffix">人</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group half">
|
||||
<view class="field-label">
|
||||
<text class="label-text">取消政策</text>
|
||||
</view>
|
||||
<picker :range="policyOptions" range-key="label" :value="policyIndex" @change="onPolicyChange">
|
||||
<view class="field-picker">
|
||||
<text class="picker-text">{{ policyOptions[policyIndex].label }}</text>
|
||||
<text class="picker-arrow">▼</text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit-btn" :disabled="loading" @tap="handleSubmit">
|
||||
{{ loading ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
<!-- 房源图片 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">房源图片</text>
|
||||
<text class="card-subtitle">{{ imageList.length }}/20</text>
|
||||
</view>
|
||||
|
||||
<view class="image-grid">
|
||||
<view v-for="(img, i) in imageList" :key="i" class="image-box">
|
||||
<image class="room-image" :src="img" mode="aspectFill" />
|
||||
<view v-if="i === 0" class="cover-badge">封面</view>
|
||||
<view class="delete-btn" @tap="removeImage(i)">
|
||||
<text class="delete-icon">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="imageList.length < 20" class="image-upload" @tap="chooseImage">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">添加图片</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="field-hint">第一张为封面图,建议上传至少3张图片</text>
|
||||
</view>
|
||||
|
||||
<!-- 设施服务 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">设施服务</text>
|
||||
<text class="card-subtitle">已选 {{ form.facilities.length }}</text>
|
||||
</view>
|
||||
|
||||
<view class="facility-grid">
|
||||
<view
|
||||
v-for="f in facilityOptions"
|
||||
:key="f"
|
||||
:class="['facility-item', { selected: form.facilities.includes(f) }]"
|
||||
@tap="toggleFacility(f)"
|
||||
>
|
||||
<text class="facility-icon">{{ getFacilityIcon(f) }}</text>
|
||||
<text class="facility-name">{{ f }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 房源描述 -->
|
||||
<view class="form-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">房源描述</text>
|
||||
</view>
|
||||
|
||||
<textarea
|
||||
class="field-textarea"
|
||||
v-model="form.description"
|
||||
placeholder="介绍房源的特色、周边环境、交通等信息,让客人更了解您的房源"
|
||||
maxlength="1000"
|
||||
/>
|
||||
<text class="field-hint">{{ form.description.length }}/1000</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-bar">
|
||||
<button class="submit-button" :disabled="loading" @tap="handleSubmit">
|
||||
{{ loading ? '保存中...' : '保存房源' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -139,6 +210,24 @@ const form = ref({
|
||||
description: '',
|
||||
});
|
||||
|
||||
function getFacilityIcon(facility: string): string {
|
||||
const iconMap: Record<string, string> = {
|
||||
'WiFi': '📶',
|
||||
'空调': '❄️',
|
||||
'热水': '🚿',
|
||||
'电视': '📺',
|
||||
'冰箱': '🧊',
|
||||
'洗衣机': '🧺',
|
||||
'停车场': '🅿️',
|
||||
'电梯': '🛗',
|
||||
'厨房': '🍳',
|
||||
'阳台': '🪴',
|
||||
'泳池': '🏊',
|
||||
'健身房': '💪',
|
||||
};
|
||||
return iconMap[facility] || '✓';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages();
|
||||
const page = pages[pages.length - 1] as any;
|
||||
@@ -147,6 +236,8 @@ onMounted(() => {
|
||||
roomId.value = Number(id);
|
||||
uni.setNavigationBarTitle({ title: '编辑房源' });
|
||||
loadRoom(Number(id));
|
||||
} else {
|
||||
uni.setNavigationBarTitle({ title: '新建房源' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -201,11 +292,11 @@ function toggleFacility(f: string) {
|
||||
async function chooseImage() {
|
||||
const remaining = 20 - imageList.value.length;
|
||||
if (remaining <= 0) {
|
||||
uni.showToast({ title: '最多上传20张图片', icon: 'none' });
|
||||
uni.showToast({ title: '最多20张', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
uni.showLoading({ title: '上传中...' });
|
||||
uni.showLoading({ title: '上传中' });
|
||||
const urls = await chooseAndUpload({ count: remaining, useSellerToken: true });
|
||||
imageList.value.push(...urls);
|
||||
} catch (err: any) {
|
||||
@@ -228,6 +319,10 @@ function validate(): boolean {
|
||||
uni.showToast({ title: '请输入有效价格', icon: 'none' });
|
||||
return false;
|
||||
}
|
||||
if (imageList.value.length === 0) {
|
||||
uni.showToast({ title: '请至少上传一张图片', icon: 'none' });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -252,10 +347,14 @@ async function handleSubmit() {
|
||||
try {
|
||||
if (roomId.value) {
|
||||
await updateMerchantRoom(roomId.value, data);
|
||||
uni.showToast({ title: '更新成功', icon: 'success' });
|
||||
} else {
|
||||
await createMerchantRoom(data);
|
||||
uni.showToast({ title: '创建成功', icon: 'success' });
|
||||
}
|
||||
uni.redirectTo({ url: '/pages/seller/rooms' });
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} catch (e: any) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
@@ -269,190 +368,353 @@ async function handleSubmit() {
|
||||
|
||||
.page-room-form {
|
||||
min-height: 100vh;
|
||||
background: $bg-page;
|
||||
padding: $spacing-xl;
|
||||
padding-bottom: 160rpx;
|
||||
background: #f5f6fa;
|
||||
padding: 24rpx;
|
||||
padding-bottom: 140rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: $bg-card;
|
||||
border-radius: $radius-lg;
|
||||
padding: $spacing-xl;
|
||||
border: 1rpx solid $border-light;
|
||||
// 表单卡片
|
||||
.form-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: $spacing-2xl;
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 28rpx;
|
||||
padding-bottom: 20rpx;
|
||||
border-bottom: 1rpx solid #f1f3f5;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #868e96;
|
||||
}
|
||||
|
||||
// 表单组
|
||||
.form-group {
|
||||
margin-bottom: 28rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.half {
|
||||
flex: 1;
|
||||
}
|
||||
&.third {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: $spacing-md;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: $font-base;
|
||||
color: $text-primary;
|
||||
margin-bottom: $spacing-sm;
|
||||
display: block;
|
||||
font-weight: $font-medium;
|
||||
|
||||
&.required::before {
|
||||
content: '*';
|
||||
color: $primary-color;
|
||||
margin-right: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-base;
|
||||
padding: 0 $spacing-lg;
|
||||
font-size: $font-base;
|
||||
border: 2rpx solid $border-light;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:focus {
|
||||
border-color: $primary-color;
|
||||
background: $bg-card;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
height: 80rpx;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-base;
|
||||
padding: 0 $spacing-lg;
|
||||
font-size: $font-base;
|
||||
.field-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 2rpx solid $border-light;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
.label-text {
|
||||
font-size: 28rpx;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.label-required {
|
||||
color: #fa5252;
|
||||
margin-left: 4rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
// 输入框
|
||||
.field-input {
|
||||
width: 100%;
|
||||
height: 160rpx;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-base;
|
||||
padding: $spacing-lg;
|
||||
font-size: $font-base;
|
||||
border: 2rpx solid $border-light;
|
||||
transition: all 0.2s ease;
|
||||
height: 80rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #212529;
|
||||
border: 1rpx solid #e9ecef;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: $primary-color;
|
||||
background: $bg-card;
|
||||
border-color: #4263eb;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&.with-prefix {
|
||||
padding-left: 56rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: $font-xs;
|
||||
color: $text-secondary;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.image-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
.field-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border: 1rpx solid #e9ecef;
|
||||
transition: all 0.2s;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus-within {
|
||||
border-color: #4263eb;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
.input-prefix,
|
||||
.input-suffix {
|
||||
flex-shrink: 0;
|
||||
font-size: 26rpx;
|
||||
color: #868e96;
|
||||
padding: 0 16rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
// 选择器
|
||||
.field-picker {
|
||||
height: 80rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 0 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border: 1rpx solid #e9ecef;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
font-size: 28rpx;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.picker-arrow {
|
||||
font-size: 20rpx;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
// 文本域
|
||||
.field-textarea {
|
||||
width: 100%;
|
||||
min-height: 200rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: #212529;
|
||||
border: 1rpx solid #e9ecef;
|
||||
line-height: 1.6;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: #4263eb;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #868e96;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
// 图片网格
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.image-box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
border-radius: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.room-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: $radius-base;
|
||||
border: 1rpx solid $border-light;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.image-delete {
|
||||
.cover-badge {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: -12rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
background: $error-color;
|
||||
border-radius: 50%;
|
||||
top: 8rpx;
|
||||
left: 8rpx;
|
||||
background: rgba(66, 99, 235, 0.9);
|
||||
color: #fff;
|
||||
font-size: $font-xs;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
backdrop-filter: blur(4rpx);
|
||||
|
||||
.image-add {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-base;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed $border-base;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 48rpx;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.facility-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $spacing-sm;
|
||||
}
|
||||
|
||||
.facility-tag {
|
||||
padding: 10rpx $spacing-xl;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-base;
|
||||
font-size: $font-xs;
|
||||
color: $text-secondary;
|
||||
border: 2rpx solid $border-light;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.active {
|
||||
background: $primary-bg;
|
||||
color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
&:active {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
.delete-icon {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.image-upload {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 100%;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
border: 2rpx dashed #dee2e6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:active {
|
||||
background: #e9ecef;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -70%);
|
||||
font-size: 56rpx;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
.upload-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 40%);
|
||||
font-size: 22rpx;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
// 设施网格
|
||||
.facility-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.facility-item {
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
border: 2rpx solid #e9ecef;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #e7f5ff;
|
||||
border-color: #4263eb;
|
||||
|
||||
.facility-name {
|
||||
color: #4263eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.facility-icon {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.facility-name {
|
||||
font-size: 24rpx;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
// 底部按钮
|
||||
.bottom-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100rpx;
|
||||
background: $primary-color;
|
||||
background: #fff;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: linear-gradient(135deg, #4c6ef5 0%, #4263eb 100%);
|
||||
color: #fff;
|
||||
border-radius: 0;
|
||||
font-size: $font-lg;
|
||||
font-weight: $font-semibold;
|
||||
border-radius: 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 8rpx 20rpx rgba(66, 99, 235, 0.25);
|
||||
letter-spacing: 2rpx;
|
||||
|
||||
&:active:not([disabled]) {
|
||||
background: $primary-dark;
|
||||
background: linear-gradient(135deg, #3b5bdb 0%, #364fc7 100%);
|
||||
transform: translateY(2rpx);
|
||||
box-shadow: 0 4rpx 12rpx rgba(66, 99, 235, 0.3);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background: $bg-disabled;
|
||||
color: $text-disabled;
|
||||
background: #e9ecef;
|
||||
color: #adb5bd;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,6 +4,24 @@
|
||||
<view class="form-section">
|
||||
<view class="section-title">基本信息</view>
|
||||
|
||||
<!-- 店铺类型 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">店铺类型</text>
|
||||
<view class="type-grid">
|
||||
<view
|
||||
v-for="type in shopTypes"
|
||||
:key="type.value"
|
||||
class="type-item"
|
||||
:class="{ active: form.shopType === type.value }"
|
||||
@tap="form.shopType = type.value"
|
||||
>
|
||||
<text class="type-emoji">{{ type.emoji }}</text>
|
||||
<text class="type-label">{{ type.label }}</text>
|
||||
<text class="type-desc">{{ type.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 店铺名称 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">店铺名称</text>
|
||||
@@ -62,6 +80,22 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 店铺封面 -->
|
||||
<view class="form-item">
|
||||
<text class="label">店铺封面</text>
|
||||
<view class="section-desc" style="margin-bottom: 24rpx;">用于店铺列表展示</view>
|
||||
<view class="upload-section">
|
||||
<view v-if="form.coverImage" class="image-preview cover-preview">
|
||||
<image class="preview-img" :src="form.coverImage" mode="aspectFill" />
|
||||
<view class="delete-btn" @tap="form.coverImage = ''">×</view>
|
||||
</view>
|
||||
<view v-else class="upload-btn cover-upload" @tap="uploadCoverImage">
|
||||
<text class="upload-icon">+</text>
|
||||
<text class="upload-text">上传店铺封面</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 门店营业执照 -->
|
||||
<view class="form-item">
|
||||
<text class="label required">门店营业执照</text>
|
||||
@@ -80,7 +114,7 @@
|
||||
|
||||
<!-- 酒店照片 -->
|
||||
<view class="form-section">
|
||||
<view class="section-title">酒店照片</view>
|
||||
<view class="section-title">店铺照片</view>
|
||||
<view class="section-desc">用于店铺首页轮播图展示,最多上传9张</view>
|
||||
|
||||
<view class="form-item">
|
||||
@@ -384,65 +418,174 @@
|
||||
</view>
|
||||
|
||||
<button class="submit-btn" :disabled="loading" @tap="handleSubmit">
|
||||
{{ loading ? '提交中...' : '提交申请' }}
|
||||
{{ submitButtonText }}
|
||||
</button>
|
||||
|
||||
<view class="tips">
|
||||
<view class="tips-title">温馨提示</view>
|
||||
<text class="tips-text">1. 提交后需等待平台审核,审核结果将通过消息通知</text>
|
||||
<text class="tips-text">2. 所有证件照片需清晰可见,确保信息真实有效</text>
|
||||
<text class="tips-text">3. 审核通过后即可发布房源开始营业</text>
|
||||
<text v-for="(tip, index) in tipsText" :key="index" class="tips-text">{{ tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { applyMerchant } from '@/api/seller/merchant';
|
||||
import { ref, computed } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { applyMerchant, updateMerchant, getMyMerchant } from '@/api/seller/merchant';
|
||||
import { useSellerStore } from '@/store/seller';
|
||||
import RegionSelector from '@/components/RegionSelector.vue';
|
||||
import { chooseAndUpload } from '@/utils/upload';
|
||||
import type { ShopFormData, RegionChangeData, ShopTypeOption } from '@/types/shop';
|
||||
|
||||
const sellerStore = useSellerStore();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const form = ref({
|
||||
// 判断是编辑模式还是创建模式
|
||||
const isEditMode = ref(false);
|
||||
|
||||
onLoad(async (options) => {
|
||||
// 通过 URL 参数判断模式
|
||||
if (options?.mode === 'edit') {
|
||||
isEditMode.value = true;
|
||||
// 加载店铺数据
|
||||
await loadMerchantData();
|
||||
}
|
||||
});
|
||||
|
||||
// 动态标题
|
||||
const pageTitle = computed(() => isEditMode.value ? '修改店铺' : '创建店铺');
|
||||
const submitButtonText = computed(() => {
|
||||
if (loading.value) return '提交中...';
|
||||
return isEditMode.value ? '保存修改' : '提交申请';
|
||||
});
|
||||
const tipsText = computed(() => {
|
||||
if (isEditMode.value) {
|
||||
return [
|
||||
'1. 修改店铺信息后需要重新等待平台审核',
|
||||
'2. 所有证件照片需清晰可见,确保信息真实有效',
|
||||
'3. 审核通过后即可继续营业'
|
||||
];
|
||||
}
|
||||
return [
|
||||
'1. 提交后需等待平台审核,审核结果将通过消息通知',
|
||||
'2. 所有证件照片需清晰可见,确保信息真实有效',
|
||||
'3. 审核通过后即可发布房源开始营业'
|
||||
];
|
||||
});
|
||||
|
||||
// 加载商家数据
|
||||
async function loadMerchantData() {
|
||||
try {
|
||||
const res = await getMyMerchant();
|
||||
const merchant = res.data;
|
||||
form.value = {
|
||||
// 基本信息
|
||||
shopName: merchant.shopName || '',
|
||||
shopType: merchant.shopType || 'hotel',
|
||||
phone: merchant.phone || '',
|
||||
province: merchant.province || '',
|
||||
city: merchant.city || '',
|
||||
district: merchant.district || '',
|
||||
address: merchant.address || '',
|
||||
description: merchant.description || '',
|
||||
coverImage: merchant.coverImage || '',
|
||||
storeLicense: merchant.storeLicense || '',
|
||||
|
||||
// 酒店照片
|
||||
hotelImages: merchant.hotelImages ? merchant.hotelImages.split(',').filter((img: string) => img) : [],
|
||||
|
||||
// 签约资料
|
||||
contractType: merchant.contractType || 'personal',
|
||||
idCardFront: merchant.idCardFront || '',
|
||||
idCardBack: merchant.idCardBack || '',
|
||||
legalIdCardFront: merchant.legalIdCardFront || '',
|
||||
legalIdCardBack: merchant.legalIdCardBack || '',
|
||||
businessLicense: merchant.businessLicense || '',
|
||||
licenseNo: merchant.licenseNo || '',
|
||||
legalPerson: merchant.legalPerson || '',
|
||||
|
||||
// 财务信息
|
||||
accountType: merchant.accountType || 'company',
|
||||
accountName: merchant.accountName || '',
|
||||
bankAccount: merchant.bankAccount || '',
|
||||
bankName: merchant.bankName || '',
|
||||
bankBranch: merchant.bankBranch || '',
|
||||
bankLicense: merchant.bankLicense || '',
|
||||
accountIdCardFront: merchant.accountIdCardFront || '',
|
||||
accountIdCardBack: merchant.accountIdCardBack || '',
|
||||
};
|
||||
} catch (error) {
|
||||
uni.showToast({ title: '获取店铺信息失败', icon: 'none' });
|
||||
}
|
||||
}
|
||||
|
||||
// 店铺类型选项
|
||||
const shopTypes: ShopTypeOption[] = [
|
||||
{
|
||||
value: 'hotel',
|
||||
label: '精品酒店',
|
||||
emoji: '🏨',
|
||||
desc: '舒适商务之选',
|
||||
},
|
||||
{
|
||||
value: 'homestay',
|
||||
label: '特色民宿',
|
||||
emoji: '🏡',
|
||||
desc: '体验当地生活',
|
||||
},
|
||||
{
|
||||
value: 'apartment',
|
||||
label: '品质公寓',
|
||||
emoji: '🏢',
|
||||
desc: '长住更优惠',
|
||||
},
|
||||
{
|
||||
value: 'hostel',
|
||||
label: '青年旅舍',
|
||||
emoji: '🎒',
|
||||
desc: '结识新朋友',
|
||||
},
|
||||
];
|
||||
|
||||
const form = ref<ShopFormData>({
|
||||
// 基本信息
|
||||
shopName: '',
|
||||
shopType: 'hotel',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
address: '',
|
||||
description: '',
|
||||
storeLicense: '', // 门店营业执照
|
||||
coverImage: '',
|
||||
storeLicense: '',
|
||||
|
||||
// 酒店照片
|
||||
hotelImages: [] as string[],
|
||||
hotelImages: [],
|
||||
|
||||
// 签约资料
|
||||
contractType: 'personal' as 'personal' | 'company',
|
||||
idCardFront: '', // 个人身份证正面
|
||||
idCardBack: '', // 个人身份证反面
|
||||
legalIdCardFront: '', // 法人身份证正面
|
||||
legalIdCardBack: '', // 法人身份证反面
|
||||
businessLicense: '', // 营业执照
|
||||
contractType: 'personal',
|
||||
idCardFront: '',
|
||||
idCardBack: '',
|
||||
legalIdCardFront: '',
|
||||
legalIdCardBack: '',
|
||||
businessLicense: '',
|
||||
licenseNo: '',
|
||||
legalPerson: '',
|
||||
|
||||
// 财务信息
|
||||
accountType: 'company' as 'company' | 'personal',
|
||||
accountName: '', // 开户名(对私)
|
||||
bankAccount: '', // 银行账号
|
||||
bankName: '', // 银行名称
|
||||
bankBranch: '', // 支行信息(对私)
|
||||
bankLicense: '', // 开户营业执照(对公)
|
||||
accountIdCardFront: '', // 开户身份证正面(对私)
|
||||
accountIdCardBack: '', // 开户身份证反面(对私)
|
||||
accountType: 'company',
|
||||
accountName: '',
|
||||
bankAccount: '',
|
||||
bankName: '',
|
||||
bankBranch: '',
|
||||
bankLicense: '',
|
||||
accountIdCardFront: '',
|
||||
accountIdCardBack: '',
|
||||
});
|
||||
|
||||
function onRegionChange(value: { province: string; city: string; district: string }) {
|
||||
function onRegionChange(value: RegionChangeData) {
|
||||
form.value.province = value.province;
|
||||
form.value.city = value.city;
|
||||
form.value.district = value.district;
|
||||
@@ -467,6 +610,20 @@ function deleteHotelImage(index: number) {
|
||||
form.value.hotelImages.splice(index, 1);
|
||||
}
|
||||
|
||||
// 上传店铺封面
|
||||
async function uploadCoverImage() {
|
||||
try {
|
||||
uni.showLoading({ title: '上传中...' });
|
||||
const urls = await chooseAndUpload({ count: 1, useSellerToken: true });
|
||||
form.value.coverImage = urls[0];
|
||||
uni.showToast({ title: '上传成功', icon: 'success' });
|
||||
} catch (err: any) {
|
||||
uni.showToast({ title: err.message || '上传失败', icon: 'none' });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
// 上传门店营业执照
|
||||
async function uploadStoreLicense() {
|
||||
try {
|
||||
@@ -687,51 +844,78 @@ async function handleSubmit() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
const res = await applyMerchant({
|
||||
// 基本信息
|
||||
shopName: form.value.shopName,
|
||||
phone: form.value.phone,
|
||||
province: form.value.province,
|
||||
city: form.value.city,
|
||||
district: form.value.district,
|
||||
address: form.value.address,
|
||||
description: form.value.description,
|
||||
storeLicense: form.value.storeLicense,
|
||||
if (isEditMode.value) {
|
||||
// 编辑模式:只提交 UpdateMerchantDto 允许的字段
|
||||
const res = await updateMerchant({
|
||||
shopName: form.value.shopName,
|
||||
coverImage: form.value.coverImage,
|
||||
phone: form.value.phone,
|
||||
description: form.value.description,
|
||||
province: form.value.province,
|
||||
city: form.value.city,
|
||||
district: form.value.district,
|
||||
address: form.value.address,
|
||||
businessLicense: form.value.businessLicense,
|
||||
licenseNo: form.value.licenseNo,
|
||||
legalPerson: form.value.legalPerson,
|
||||
});
|
||||
|
||||
// 酒店照片
|
||||
hotelImages: form.value.hotelImages.join(','),
|
||||
uni.showToast({ title: '修改成功', icon: 'success' });
|
||||
|
||||
// 签约资料
|
||||
contractType: form.value.contractType,
|
||||
idCardFront: form.value.idCardFront,
|
||||
idCardBack: form.value.idCardBack,
|
||||
legalIdCardFront: form.value.legalIdCardFront,
|
||||
legalIdCardBack: form.value.legalIdCardBack,
|
||||
businessLicense: form.value.businessLicense,
|
||||
licenseNo: form.value.licenseNo,
|
||||
legalPerson: form.value.legalPerson,
|
||||
// 更新 sellerInfo 中的 merchantStatus 为 pending
|
||||
if (sellerStore.sellerInfo && res.data.status === 'pending') {
|
||||
sellerStore.sellerInfo.merchantStatus = 'pending';
|
||||
uni.setStorageSync('sellerInfo', sellerStore.sellerInfo);
|
||||
}
|
||||
} else {
|
||||
// 创建模式:调用申请接口
|
||||
const res = await applyMerchant({
|
||||
// 基本信息
|
||||
shopName: form.value.shopName,
|
||||
phone: form.value.phone,
|
||||
province: form.value.province,
|
||||
city: form.value.city,
|
||||
district: form.value.district,
|
||||
address: form.value.address,
|
||||
description: form.value.description,
|
||||
coverImage: form.value.coverImage,
|
||||
storeLicense: form.value.storeLicense,
|
||||
|
||||
// 财务信息
|
||||
accountType: form.value.accountType,
|
||||
accountName: form.value.accountName,
|
||||
bankAccount: form.value.bankAccount,
|
||||
bankName: form.value.bankName,
|
||||
bankBranch: form.value.bankBranch,
|
||||
bankLicense: form.value.bankLicense,
|
||||
accountIdCardFront: form.value.accountIdCardFront,
|
||||
accountIdCardBack: form.value.accountIdCardBack,
|
||||
});
|
||||
// 酒店照片
|
||||
hotelImages: form.value.hotelImages.join(','),
|
||||
|
||||
uni.showToast({ title: '提交成功', icon: 'success' });
|
||||
// 签约资料
|
||||
contractType: form.value.contractType,
|
||||
idCardFront: form.value.idCardFront,
|
||||
idCardBack: form.value.idCardBack,
|
||||
legalIdCardFront: form.value.legalIdCardFront,
|
||||
legalIdCardBack: form.value.legalIdCardBack,
|
||||
businessLicense: form.value.businessLicense,
|
||||
licenseNo: form.value.licenseNo,
|
||||
legalPerson: form.value.legalPerson,
|
||||
|
||||
// 通过 store 方法更新商家信息
|
||||
sellerStore.updateMerchantInfo(res.data.id, 'pending');
|
||||
// 财务信息
|
||||
accountType: form.value.accountType,
|
||||
accountName: form.value.accountName,
|
||||
bankAccount: form.value.bankAccount,
|
||||
bankName: form.value.bankName,
|
||||
bankBranch: form.value.bankBranch,
|
||||
bankLicense: form.value.bankLicense,
|
||||
accountIdCardFront: form.value.accountIdCardFront,
|
||||
accountIdCardBack: form.value.accountIdCardBack,
|
||||
});
|
||||
|
||||
uni.showToast({ title: '提交成功', icon: 'success' });
|
||||
|
||||
// 通过 store 方法更新商家信息
|
||||
sellerStore.updateMerchantInfo(res.data.id, 'pending');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
uni.redirectTo({ url: '/pages/seller/home' });
|
||||
}, 1500);
|
||||
} catch (error: any) {
|
||||
uni.showToast({ title: error.message || '提交失败', icon: 'none' });
|
||||
uni.showToast({ title: error.message || (isEditMode.value ? '修改失败' : '提交失败'), icon: 'none' });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -876,6 +1060,63 @@ async function handleSubmit() {
|
||||
background: $primary-color;
|
||||
}
|
||||
|
||||
/* 店铺类型选择 */
|
||||
.type-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-md;
|
||||
}
|
||||
|
||||
.type-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl;
|
||||
background: $bg-page;
|
||||
border-radius: $radius-lg;
|
||||
border: 2rpx solid $border-light;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 180rpx;
|
||||
|
||||
&.active {
|
||||
background: linear-gradient(135deg, rgba($primary-color, 0.1) 0%, rgba($primary-color, 0.05) 100%);
|
||||
border-color: $primary-color;
|
||||
box-shadow: 0 4rpx 12rpx rgba($primary-color, 0.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.type-emoji {
|
||||
font-size: 64rpx;
|
||||
line-height: 1;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.type-label {
|
||||
font-size: $font-lg;
|
||||
font-weight: $font-semibold;
|
||||
color: $text-primary;
|
||||
margin-bottom: $spacing-xs;
|
||||
|
||||
.type-item.active & {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.type-desc {
|
||||
font-size: $font-xs;
|
||||
color: $text-tertiary;
|
||||
text-align: center;
|
||||
|
||||
.type-item.active & {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
/* 上传区域 */
|
||||
.upload-section {
|
||||
display: flex;
|
||||
@@ -947,6 +1188,17 @@ async function handleSubmit() {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 封面图片预览 */
|
||||
.cover-preview {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
}
|
||||
|
||||
.cover-upload {
|
||||
width: 100%;
|
||||
height: 300rpx;
|
||||
}
|
||||
|
||||
/* 提交按钮 */
|
||||
.submit-btn {
|
||||
width: 100%;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ export interface Merchant {
|
||||
sellerId: number;
|
||||
shopName: string;
|
||||
logo: string;
|
||||
coverImage?: string;
|
||||
description: string;
|
||||
phone: string;
|
||||
province: string;
|
||||
@@ -30,6 +31,9 @@ export interface Merchant {
|
||||
isHot?: boolean;
|
||||
isNew?: boolean;
|
||||
facilities?: string[];
|
||||
salesCount?: number;
|
||||
images?: string[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface MerchantCardData {
|
||||
@@ -37,6 +41,9 @@ export interface MerchantCardData {
|
||||
name: string;
|
||||
coverImage: string;
|
||||
cityName: string;
|
||||
city?: string;
|
||||
district?: string;
|
||||
province?: string;
|
||||
rating: number;
|
||||
reviewCount: number;
|
||||
description: string;
|
||||
@@ -47,6 +54,7 @@ export interface MerchantCardData {
|
||||
isRecommend?: boolean;
|
||||
isHot?: boolean;
|
||||
isNew?: boolean;
|
||||
salesCount?: number;
|
||||
}
|
||||
|
||||
export interface SearchParams {
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* 签约类型
|
||||
*/
|
||||
export type ContractType = 'personal' | 'company';
|
||||
|
||||
/**
|
||||
* 账户类型
|
||||
*/
|
||||
export type AccountType = 'company' | 'personal';
|
||||
|
||||
/**
|
||||
* 店铺类型
|
||||
*/
|
||||
export type ShopType = 'hotel' | 'homestay' | 'apartment' | 'hostel';
|
||||
|
||||
/**
|
||||
* 店铺类型选项
|
||||
*/
|
||||
export interface ShopTypeOption {
|
||||
value: ShopType;
|
||||
label: string;
|
||||
emoji: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺表单数据
|
||||
*/
|
||||
export interface ShopFormData {
|
||||
// 基本信息
|
||||
shopName: string;
|
||||
shopType: ShopType; // 店铺类型
|
||||
phone: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
address: string;
|
||||
description: string;
|
||||
coverImage: string; // 店铺封面
|
||||
storeLicense: string; // 门店营业执照
|
||||
|
||||
// 酒店照片
|
||||
hotelImages: string[];
|
||||
|
||||
// 签约资料
|
||||
contractType: ContractType;
|
||||
idCardFront: string; // 个人身份证正面
|
||||
idCardBack: string; // 个人身份证反面
|
||||
legalIdCardFront: string; // 法人身份证正面
|
||||
legalIdCardBack: string; // 法人身份证反面
|
||||
businessLicense: string; // 营业执照
|
||||
licenseNo: string; // 营业执照编号
|
||||
legalPerson: string; // 法人姓名
|
||||
|
||||
// 财务信息
|
||||
accountType: AccountType;
|
||||
accountName: string; // 开户名(对私)
|
||||
bankAccount: string; // 银行账号
|
||||
bankName: string; // 银行名称
|
||||
bankBranch: string; // 支行信息(对私)
|
||||
bankLicense: string; // 开户营业执照(对公)
|
||||
accountIdCardFront: string; // 开户身份证正面(对私)
|
||||
accountIdCardBack: string; // 开户身份证反面(对私)
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺申请提交数据
|
||||
*/
|
||||
export interface ShopApplyData {
|
||||
// 基本信息
|
||||
shopName: string;
|
||||
shopType: ShopType;
|
||||
phone: string;
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
address: string;
|
||||
description: string;
|
||||
coverImage: string;
|
||||
storeLicense: string;
|
||||
|
||||
// 酒店照片(逗号分隔的字符串)
|
||||
hotelImages: string;
|
||||
|
||||
// 签约资料
|
||||
contractType: ContractType;
|
||||
idCardFront: string;
|
||||
idCardBack: string;
|
||||
legalIdCardFront: string;
|
||||
legalIdCardBack: string;
|
||||
businessLicense: string;
|
||||
licenseNo: string;
|
||||
legalPerson: string;
|
||||
|
||||
// 财务信息
|
||||
accountType: AccountType;
|
||||
accountName: string;
|
||||
bankAccount: string;
|
||||
bankName: string;
|
||||
bankBranch: string;
|
||||
bankLicense: string;
|
||||
accountIdCardFront: string;
|
||||
accountIdCardBack: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 地区选择器变化事件数据
|
||||
*/
|
||||
export interface RegionChangeData {
|
||||
province: string;
|
||||
city: string;
|
||||
district: string;
|
||||
}
|
||||
Reference in New Issue
Block a user