feat: 迭代

This commit is contained in:
2026-05-11 17:59:19 +08:00
parent 2d5598dcad
commit 554bb702a2
24 changed files with 2539 additions and 2494 deletions
+34 -2
View File
@@ -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>
+1 -7
View File
@@ -89,13 +89,7 @@
{
"path": "pages/seller/shop-create",
"style": {
"navigationBarTitleText": "创建店铺"
}
},
{
"path": "pages/seller/shop-edit",
"style": {
"navigationBarTitleText": "修改店铺"
"navigationBarTitleText": "店铺信息"
}
},
{
+484 -210
View File
@@ -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;
+131 -317
View File
@@ -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>
+39 -39
View File
@@ -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
+472 -210
View File
@@ -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">面积()</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"></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>
+312 -60
View File
@@ -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
+8
View File
@@ -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 {
+113
View File
@@ -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;
}