stores.vue 8.71 KB
<template>
  <view class="page-shell store-list-page">
    <view class="page-blur"></view>
    <view class="page-blur-secondary"></view>
    <view class="page-blur-tertiary"></view>

    <view class="page-content subpage-content">
      <view class="subpage-nav">
        <view class="subpage-back" @tap="goBack">‹</view>
        <view class="subpage-title-wrap">
          <text class="subpage-title">全部门店</text>
          <text class="subpage-subtitle">门店较多时,也能更轻松地找到适合这次到店的那一家</text>
        </view>
      </view>

      <view class="store-top-card glass-card">
        <text class="hero-kicker">STORE GUIDE</text>
        <text class="hero-title">三十余家门店,慢慢挑一间顺路又合心意的</text>
        <text class="hero-desc">可以按区域、营业状态或距离轻松筛选,也可以直接搜索门店名与所在片区。</text>
      </view>

      <view class="search-card glass-card">
        <view class="search-icon"></view>
        <input
          v-model="keyword"
          class="search-input"
          placeholder="搜索门店名、区域或街道"
          placeholder-class="search-placeholder"
          confirm-type="search"
        />
      </view>

      <scroll-view class="filter-scroll" scroll-x>
        <view class="filter-row">
          <view
            v-for="item in filters"
            :key="item.key"
            class="filter-chip"
            :class="{ active: activeFilter === item.key }"
            @tap="activeFilter = item.key"
          >
            {{ item.label }}
          </view>
        </view>
      </scroll-view>

      <view class="section-head store-head">
        <text class="section-title">为你找到 {{ filteredStores.length }} 家门店</text>
        <text class="section-link">{{ headText }}</text>
      </view>

      <view
        v-for="store in filteredStores"
        :key="store.id"
        class="store-list-card glass-card"
        @tap="goDetail(store)"
      >
        <image class="store-thumb" :src="store.cover" mode="aspectFill" />
        <view class="store-main">
          <view class="store-title-row">
            <text class="store-name">{{ store.name }}</text>
            <text class="store-distance">{{ store.distance }}</text>
          </view>
          <view class="store-tag-row">
            <text class="store-chip">{{ store.district }}</text>
            <text class="store-chip rose">{{ store.openStatus }}</text>
            <text class="store-chip">{{ store.vacancy }}</text>
          </view>
          <text class="store-address">{{ store.address }}</text>
          <text class="store-meta">营业时间 {{ store.hours }}</text>
          <view class="store-actions">
            <view class="mini-button" @tap.stop="openMap(store)">导航</view>
            <view class="mini-button primary-mini" @tap.stop="bookStore(store)">预约</view>
          </view>
        </view>
      </view>

      <view v-if="!filteredStores.length" class="subpage-card">
        <text class="empty-tip">暂时没有找到合适的门店,换个区域或关键词再看看。</text>
      </view>
    </view>
  </view>
</template>

<script>
import { stores } from '@/data/mock'

export default {
  data() {
    return {
      stores,
      keyword: '',
      activeFilter: 'all',
      filters: [
        { key: 'all', label: '全部' },
        { key: 'near', label: '离我较近' },
        { key: 'open', label: '营业中' },
        { key: '锦江', label: '锦江' },
        { key: '武侯', label: '武侯' },
        { key: '高新', label: '高新' }
      ]
    }
  },
  computed: {
    filteredStores() {
      let list = this.stores.slice()
      if (this.activeFilter === 'near') {
        list = list.filter(item => parseFloat(item.distance) <= 3.5)
      } else if (this.activeFilter === 'open') {
        list = list.filter(item => item.openStatus === '营业中')
      } else if (this.activeFilter !== 'all') {
        list = list.filter(item => item.district === this.activeFilter)
      }

      const keyword = String(this.keyword || '').trim()
      if (!keyword) {
        return list
      }
      return list.filter(item =>
        item.name.indexOf(keyword) > -1 ||
        item.district.indexOf(keyword) > -1 ||
        item.address.indexOf(keyword) > -1
      )
    },
    headText() {
      if (this.activeFilter === 'near') {
        return '离你更近'
      }
      if (this.activeFilter === 'open') {
        return '正在营业'
      }
      if (this.activeFilter !== 'all') {
        return `${this.activeFilter}片区`
      }
      return '可按区域筛选'
    }
  },
  methods: {
    goBack() {
      uni.navigateBack({
        fail: () => {
          uni.redirectTo({ url: '/pages/home/home' })
        }
      })
    },
    goDetail(store) {
      uni.navigateTo({
        url: `/packageBooking/store-detail/store-detail?id=${store.id}`
      })
    },
    openMap(store) {
      uni.showToast({ title: `已为你准备 ${store.name} 导航`, icon: 'none' })
    },
    bookStore(store) {
      uni.redirectTo({ url: `/pages/booking/booking?storeId=${store.id}` })
    }
  }
}
</script>

<style lang="scss" scoped>
@import '@/styles/mixins.scss';
@import '@/styles/subpage.scss';

.store-top-card {
  padding: 34rpx;
  margin-bottom: 24rpx;
  transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 340ms ease;
}

.store-top-card:active {
  transform: translateY(-4rpx);
}

.search-card {
  display: flex;
  align-items: center;
  padding: 0 24rpx;
  min-height: 92rpx;
  margin-bottom: 20rpx;
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 320ms ease;
}

.search-card:active {
  transform: translateY(-3rpx);
}

.search-icon {
  width: 34rpx;
  height: 34rpx;
  margin-right: 16rpx;
  border: 3rpx solid rgba(100, 140, 118, 0.52);
  border-radius: 50%;
  position: relative;
}

.search-icon::after {
  content: '';
  position: absolute;
  right: -10rpx;
  bottom: -7rpx;
  width: 14rpx;
  height: 3rpx;
  border-radius: 999rpx;
  background: rgba(100, 140, 118, 0.52);
  transform: rotate(45deg);
}

.search-input {
  flex: 1;
  height: 92rpx;
  font-size: 26rpx;
  color: $text-primary;
}

.search-placeholder {
  color: $text-tertiary;
}

.filter-scroll {
  white-space: nowrap;
  margin-bottom: 20rpx;
}

.filter-row {
  display: inline-flex;
  padding-bottom: 6rpx;
}

.filter-chip {
  @include selection-chip;
  min-width: 136rpx;
  padding: 20rpx 26rpx;
  margin-right: 16rpx;
  text-align: center;
  font-size: 24rpx;
  color: $text-secondary;
  transition: transform 240ms ease, box-shadow 240ms ease, background 240ms ease, color 240ms ease;
}

.filter-chip:active {
  transform: translateY(-2rpx) scale(0.98);
}

.filter-chip.active {
  @include selection-chip-active;
}

.store-head {
  margin-bottom: 20rpx;
}

.store-list-card {
  display: flex;
  padding: 24rpx;
  margin-bottom: 18rpx;
  transition: transform 340ms cubic-bezier(0.22, 1, 0.36, 1), box-shadow 340ms ease;
  animation: lfRevealUp 760ms cubic-bezier(0.22, 1, 0.36, 1) both;
}

.store-list-card:nth-of-type(1) { animation-delay: 0.08s; }
.store-list-card:nth-of-type(2) { animation-delay: 0.16s; }
.store-list-card:nth-of-type(3) { animation-delay: 0.24s; }
.store-list-card:nth-of-type(4) { animation-delay: 0.32s; }

.store-list-card:active {
  transform: translateY(-5rpx);
}

.store-thumb {
  width: 176rpx;
  height: 176rpx;
  border-radius: 28rpx;
  flex-shrink: 0;
  transition: transform 1.1s ease;
}

.store-list-card:active .store-thumb {
  transform: scale(1.04);
}

.store-main {
  flex: 1;
  margin-left: 20rpx;
}

.store-title-row {
  display: flex;
  justify-content: space-between;
  gap: 16rpx;
  align-items: flex-start;
}

.store-name {
  flex: 1;
  font-size: 30rpx;
  line-height: 1.45;
  font-weight: 700;
  color: $text-primary;
}

.store-distance {
  font-size: 22rpx;
  color: $brand-accent;
  white-space: nowrap;
}

.store-tag-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10rpx;
  margin-top: 12rpx;
}

.store-chip {
  @include chip-soft;
  padding: 8rpx 16rpx;
  font-size: 20rpx;
  transition: transform 240ms ease;
}

.store-chip.rose {
  @include chip-rose;
  padding: 8rpx 16rpx;
  font-size: 20rpx;
}

.store-address,
.store-meta {
  display: block;
  font-size: 23rpx;
  line-height: 1.7;
  color: $text-secondary;
}

.store-address {
  margin-top: 14rpx;
}

.store-meta {
  margin-top: 4rpx;
}

.store-actions {
  display: flex;
  gap: 12rpx;
  margin-top: 16rpx;
}

.mini-button {
  @include secondary-button;
  flex: 1;
  min-width: 0;
  height: 70rpx;
  font-size: 24rpx;
  letter-spacing: 1rpx;
  transition: transform 240ms ease, box-shadow 240ms ease;
}

.mini-button:active {
  transform: scale(0.97);
  box-shadow: 0 10rpx 20rpx rgba(54, 77, 64, 0.08);
}

.primary-mini {
  color: #fff;
  background: $brand-primary-deep;
}
</style>