初始化

This commit is contained in:
leiking
2026-06-29 10:54:33 +08:00
parent 761cee968e
commit 4983006317
156 changed files with 25687 additions and 0 deletions
+341
View File
@@ -0,0 +1,341 @@
<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>