Files
beauty-miniapp-uni/pages/home/index.vue
T
2026-06-29 10:54:33 +08:00

342 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<view class="card hero" @tap="goStore">
<view class="row between">
<view>
<view class="trow">
<image v-if="store.logo" class="logo" :src="store.logo" mode="aspectFill" />
<view class="title">{{ store.name }}</view>
</view>
<view class="sub muted">{{ store.openHours }} · {{ store.address }}</view>
</view>
<view class="pill" @tap.stop="callStore">
<text class="p-t">电话</text>
</view>
</view>
<view class="locrow row between">
<view class="loc muted">{{ locText }} · {{ distText }}</view>
<view class="nav" @tap.stop="navToStore">导航</view>
</view>
</view>
<view class="card banner">
<swiper class="sw" circular autoplay interval="3500" duration="500">
<swiper-item>
<view class="b b1">
<view class="b-t">新客体验</view>
<view class="b-s">水氧净透 ¥99</view>
</view>
</swiper-item>
<swiper-item>
<view class="b b2">
<view class="b-t">皮肤管理</view>
<view class="b-s">补水修护 ¥238</view>
</view>
</swiper-item>
<swiper-item>
<view class="b b3">
<view class="b-t">次卡特惠</view>
<view class="b-s">5 ¥899</view>
</view>
</swiper-item>
</swiper>
</view>
<view class="grid4">
<view class="card q" @tap="goBooking">
<view class="q-t">立即预约</view>
<view class="q-s muted">选时间/技师</view>
</view>
<view class="card q" @tap="goProjects">
<view class="q-t">全部项目</view>
<view class="q-s muted">价格/时长</view>
</view>
<view class="card q" @tap="goCoupons">
<view class="q-t">我的次卡</view>
<view class="q-s muted">剩余次数</view>
</view>
<view class="card q" @tap="goMember">
<view class="q-t">会员中心</view>
<view class="q-s muted">积分/储值</view>
</view>
</view>
<view class="section row between">
<view class="h">人气推荐项目</view>
<view class="more" @tap="goProjects">全部</view>
</view>
<view class="grid2">
<ProjectGridCard v-for="p in hotProjects" :key="p.id" :project="p" />
</view>
<view class="card store">
<view class="row between">
<view class="sh">门店信息</view>
<view class="more" @tap="goStore">查看</view>
</view>
<view class="muted sline">营业时间{{ store.openHours }}休息{{ store.restDay }}</view>
<view class="muted sline">地址{{ store.address }}</view>
<view class="muted sline">电话{{ store.phone }}</view>
<view class="row between sline">
<view class="muted">技师团队点击查看</view>
<view class="rt">
<view class="star"> {{ teamRating }}</view>
<view class="like" @tap.stop="likeTeam"> {{ likeCount }}</view>
</view>
</view>
</view>
<AiFloat />
</view>
</template>
<script>
import AiFloat from '@/components/AiFloat.vue'
import ProjectGridCard from '@/components/ProjectGridCard.vue'
import { storeProfile, projects } from '@/common/mockData'
export default {
components: { AiFloat, ProjectGridCard },
data() {
return {
store: storeProfile,
hotProjects: projects.slice(0, 4),
locText: '定位:获取中…',
distText: '距离:计算中…',
teamRating: 4.9,
likeCount: 128
}
},
onShow() {
this.locText = '定位:获取中…'
this.distText = '距离:计算中…'
uni.getLocation({
type: 'gcj02',
success: (res) => {
this.locText = '定位:上海市 静安区(示例)'
const d = this.calcDistanceMeter(res.latitude, res.longitude, this.store.latitude, this.store.longitude)
this.distText = `距离:${this.formatDistance(d)}`
},
fail: () => {
this.locText = '定位:未开启(可在“我的”页授权定位)'
this.distText = '距离:--'
}
})
},
methods: {
goProjects() {
uni.switchTab({ url: '/pages/projects/list' })
},
goBooking() {
const p = Array.isArray(projects) && projects.length ? projects[0] : null
if (!p) {
this.goProjects()
return
}
uni.navigateTo({ url: `/pages/booking/create?projectId=${p.id}` })
},
goCoupons() {
uni.navigateTo({ url: '/pages/coupons/list' })
},
goMember() {
uni.switchTab({ url: '/pages/member/index' })
},
goStore() {
uni.navigateTo({ url: '/pages/store/detail' })
},
callStore() {
uni.makePhoneCall({ phoneNumber: this.store.phone })
},
navToStore() {
const lat = this.store.latitude
const lng = this.store.longitude
if (typeof lat !== 'number' || typeof lng !== 'number') {
uni.showToast({ title: '门店坐标未配置', icon: 'none' })
return
}
uni.openLocation({
latitude: lat,
longitude: lng,
name: this.store.name,
address: this.store.address
})
},
calcDistanceMeter(lat1, lng1, lat2, lng2) {
if ([lat1, lng1, lat2, lng2].some((x) => typeof x !== 'number')) return NaN
const toRad = (v) => (v * Math.PI) / 180
const R = 6371000
const dLat = toRad(lat2 - lat1)
const dLng = toRad(lng2 - lng1)
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2)
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
},
formatDistance(m) {
if (!Number.isFinite(m)) return '--'
if (m < 1000) return `${Math.max(1, Math.round(m))}m`
const km = m / 1000
if (km < 10) return `${km.toFixed(1)}km`
return `${Math.round(km)}km`
},
likeTeam() {
this.likeCount += 1
uni.showToast({ title: '已点赞', icon: 'none' })
}
}
}
</script>
<style lang="scss" scoped>
.hero {
padding: 26rpx;
}
.trow {
display: flex;
align-items: center;
gap: 14rpx;
}
.logo {
width: 56rpx;
height: 56rpx;
border-radius: 16rpx;
}
.title {
font-size: 40rpx;
font-weight: 950;
}
.sub {
margin-top: 8rpx;
font-size: 26rpx;
}
.loc {
font-size: 24rpx;
}
.locrow {
margin-top: 14rpx;
}
.nav {
padding: 12rpx 16rpx;
border-radius: 999rpx;
background: rgba(59, 130, 246, 0.14);
color: #1d4ed8;
font-size: 24rpx;
font-weight: 900;
}
.pill {
padding: 14rpx 18rpx;
border-radius: 999rpx;
background: rgba(17, 24, 39, 0.06);
}
.p-t {
font-weight: 700;
font-size: 26rpx;
}
.banner {
margin-top: 18rpx;
padding: 0;
overflow: hidden;
}
.sw {
height: 220rpx;
}
.b {
height: 220rpx;
padding: 26rpx;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.b1 {
background: linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(59, 130, 246, 1) 100%);
}
.b2 {
background: linear-gradient(135deg, rgba(3, 105, 161, 1) 0%, rgba(16, 185, 129, 1) 100%);
}
.b3 {
background: linear-gradient(135deg, rgba(124, 58, 237, 1) 0%, rgba(59, 130, 246, 1) 100%);
}
.b-t {
color: #fff;
font-weight: 950;
font-size: 42rpx;
}
.b-s {
margin-top: 10rpx;
color: rgba(255, 255, 255, 0.84);
font-size: 28rpx;
font-weight: 700;
}
.grid4 {
margin-top: 18rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 18rpx;
}
.q {
padding: 18rpx;
}
.q-t {
font-weight: 950;
font-size: 30rpx;
}
.q-s {
margin-top: 8rpx;
font-size: 24rpx;
}
.section {
margin-top: 28rpx;
padding: 6rpx 2rpx;
}
.h {
font-size: 32rpx;
font-weight: 950;
}
.more {
color: #3b82f6;
font-weight: 700;
font-size: 28rpx;
}
.grid2 {
margin-top: 14rpx;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 18rpx;
}
.store {
margin-top: 18rpx;
padding: 22rpx;
}
.rt {
display: flex;
align-items: center;
gap: 14rpx;
}
.star {
padding: 10rpx 14rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 900;
background: rgba(245, 158, 11, 0.14);
color: #b45309;
}
.like {
padding: 10rpx 14rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 900;
background: rgba(17, 24, 39, 0.06);
color: rgba(17, 24, 39, 0.86);
}
.sh {
font-size: 32rpx;
font-weight: 950;
}
.sline {
margin-top: 12rpx;
font-size: 26rpx;
}
</style>