技术框架:thinkphp+mysql+跨平台uniapp
完全开源无需授权
支持:
多门店商家入驻,管理
门店自助设备预约
电子门锁扫码开门
会员开购买,会员余额充值
自助订单和宠物预约订单管理
宠物添加管理
洗护知识文章设置
<template>
<view class="pages-home" v-if="isLoad" :style="{background:`linear-gradient(${primaryColor} 220rpx, #f5f5f5 400rpx)`}">
<view :style="{height:banner.length > 0?`100%`:`84rpx`}">
<!-- :class="[{'mt-md':banner.length ==0},{'abs':banner.length>0}]" -->
<view class="index-swiper-container">
<swiper class="index-swiper" autoplay :interval="10000">
<swiper-item v-for="(item, index) in banner" :key="index">
<view class="index-swiper-item">
<image class="index-swiper-item-img" :src="item.img"></image>
</view>
</swiper-item>
</swiper>
</view>
<view class="index-menu-container" :style="'--menu-bg:' + primaryColor">
<view class="index-menu-item one" @click="aaaaa({id:'', title:'预约服务'})">
<text class="title">预约服务</text>
<text class="text">SERVICE</text>
<text class="iconfont chongwu-1 icon"></text>
</view>
<view @click="onClickGoApply" hover-class="none" class="index-menu-item two">
<text class="title">宠托师入驻</text>
<text class="text">APPLY</text>
</view>
<navigator url="/user/pages/pet/list" hover-class="none" class="index-menu-item three">
<text class="title">我的宠物</text>
<text class="text">MY PETS</text>
</navigator>
</view>
<view class="index-grid-container">
<view class="index-grid-item"
v-for="(item, index) in classify.slice(0,8) "
:index="index"
:key="index"
@click.native='aaaaa(item)'>
<image class="index-grid-item-icon" mode="aspectFill" :src="item.img" />
<text class="index-grid-item-text">{{item.title}}</text>
</view>
</view>
<navigator url="/pages/question/question" hover-class="none">
<image class="index-apply-container" mode="widthFix" :src="imgroot + 'wenti.png'"></image>
</navigator>
</view>
<!-- <view style="margin-bottom: 2%;" :class="[{'mt-md':banner.length ==0}]">
<uni-grid :column="2" :highlight="true" class="butImg">
<uni-grid-item v-for="(item, index) in butImg" :index="index" :key="index" style="width: 45%;">
<view class="grid-item-box" style="background-color: #fff; background: url(item.loginUrl)">
<navigator :url="item.goUrl" class="but_class">
<button style="width: 100%;" type="default" plain="false"></button>
</navigator>
</view>
</uni-grid-item>
<uni-grid-item style="width: 45%;">
<view class="grid-item-box" style="background-color: #fff;">
<navigator url="/pages/technician" class="but_class1">
<image :src="btnLeftUrl" class="but_class1" style="background-color: #fff;">
</image>
</navigator>
</view>
</uni-grid-item>
<uni-grid-item style="width: 45%;">
<view class="grid-item-box" style="background-color: #fff;">
<navigator url="/technician/pages/apply" class="but_class2">
<image :src="btnRightUrl" class="but_class2" style="background-color: #fff;">
</image>
</navigator>
</view>
</uni-grid-item>
</uni-grid>
</view> -->
<!-- :class="[{'mt-md':banner.length ==0},{'abs':banner.length>0}]" -->
<!-- <view style="background: transparent;" class="search-box flex-center fill-base ml-md mr-md radius" :class="[{'mt-md':banner.length ==0}]">
<view style="width: 92%;">
<tab @change="handerTabChange" :isLine="false" :list="tabList" :activeIndex="activeIndex*1"
:activeColor="primaryColor" :width="100/tabList.length + '%'" height="84rpx"></tab>
</view>
<view style="width: 8%;"></view>
</view> -->
<!-- <view @tap.stop="goDetail(index)" class="list-item flex-center mt-md ml-md mr-md pd-lg fill-base radius-16"
v-for="(item, index) in list.data" :key="index">
<image mode="aspectFill" class="cover radius-16" :src="item.cover"></image>
<view class="flex-1 ml-md" style="max-width: 450rpx;">
<view class="flex-between">
<view class="f-title c-title text-bold max-270 ellipsis">{{ item.title }}</view>
<view class="f-caption c-caption">{{ item.total_sale }}人已预约</view>
</view>
<view class="f-caption c-caption mt-sm mb-sm ellipsis" style="height: 36rpx;">{{ item.sub_title || '' }}
</view>
<view class="flex-between mt-md">
<view class="flex-y-center f-desc c-caption max-350 ellipsis">
<view class="time-long flex-center">{{ item.time_long }}分钟</view>
<view class="flex-y-baseline f-icontext c-warning ml-sm mr-sm">¥<view class="f-sm-title">
{{ item.price }}
</view>
</view>
<view class="text-delete" v-if="item.init_price">¥{{ item.init_price }}</view>
</view>
<view class="item-btn flex-center f-caption c-base" :style="{ background: primaryColor }">
选择宠托师
</view>
</view>
</view>
</view>
<load-more :noMore="list.current_page >= list.last_page && list.data.length > 0" :loading="loading"
v-if="loading">
</load-more>
<abnor v-if="!loading && list.data.length <= 0 && list.current_page == 1"></abnor>
<view class="space-footer"></view>
<uni-popup ref="coupon_item" type="center" :maskClick="false">
<view class="coupon-popup f_r_c_c">
<image class="bg-img" src="https://lbqnyv2.migugu.com/bianzu3.png" mode=""></image>
<i @tap.stop="$refs.coupon_item.close()" class="iconfont icon-close c-base"></i>
</image>
<view class="coupon-info f_c_m_c">
<view class="tops f_c_m_c">
<view class="">
成功领取
</view>
<view class="">
卡券将放入“我的-我的卡券”
</view>
</view>
<view class="lists f_r_c_c">
<scroll-view scroll-y style="width: 420rpx;height:100%;">
<view class="list f_r_sb_c" v-for="(item, index) in couponList" :key="index">
<image src="/static/coupon/coupon.png" mode="aspectFill"></image>
<view class="f_r_sb_c">
<view class="f_c_m_c">
<view class="price">
{{item.discount}}
</view>
<view class="price_text">
{{item.full*1>0?`满${item.full}可用`:`立减`}}
</view>
</view>
<view class="title f_r_m_c">
<view class="ellipsis-3">
{{item.title}}
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<view class="btns f_r_c_c" @tap.stop="userGetCoupon">
<view class="f_r_c_c">
领取到卡包
</view>
</view>
</view>
</uni-popup> -->
<view :style="{height: `${configInfo.tabbarHeight}px`}"></view>
<tabbar :cur="0"></tabbar>
</view>
</template>
<script>
import {
mapState,
mapActions,
mapMutations
} from "vuex"
import tabbar from "@/components/tabbar.vue"
import {
imgroot,
siteroot
} from "../siteinfo"
export default {
components: {
tabbar
},
data() {
return {
imgroot,
searchValue:"",
couponList: [], //优惠券
isLoad: false,
isLoadBanner: false,
options: {},
loading: true,
lockTap: false,
btnLeftUrl: imgroot + '/attachment/image/leftBut.jpg',
btnRightUrl: imgroot + '/attachment/image/rightBut.jpg',
butImg: [{
loginUrl: "../static/leftBut.png",
goUrl: "/pages/technician",
},
{
loginUrl: "../static/rightBut.png",
goUrl: "/technician/pages/apply",
}
],
chackIndex: null
}
},
computed: mapState({
pageActive: state => state.service.pageActive,
cityId: state => state.technician.cityId,
cityIndex: state => state.technician.cityIndex,
cityList: state => state.technician.cityList,
activeIndex: state => state.service.activeIndex,
tabList: state => state.service.tabList,
param: state => state.service.param,
list: state => state.service.list,
banner: state => state.service.banner,
classify: state => state.service.classify,
classifyId: state => state.service.classifyId,
primaryColor: state => state.config.configInfo.primaryColor,
subColor: state => state.config.configInfo.subColor,
configInfo: state => state.config.configInfo,
autograph: state => state.user.autograph,
location: state => state.user.location,
userInfo: state => state.user.userInfo,
mineInfo: state => state.user.mineInfo
}),
async onLoad(options) {
this.$util.showLoading()
options = await this.updateCommonOptions(options)
this.options = options
if (this.pageActive) {
this.$util.setNavigationBarColor({
bg: this.primaryColor
})
this.isLoad = true
this.loading = false
this.$util.hideAll()
return
}
await this.initIndex()
this.updateServiceItem({
key: 'pageActive',
val: true
})
},
onReady(){
this.$util.setNavigationBarColor({
bg: this.primaryColor
})
},
onPullDownRefresh() {
// #ifndef APP-PLUS
uni.showNavigationBarLoading()
// #endif
this.initRefresh();
uni.stopPullDownRefresh()
},
onReachBottom() {
if (this.list.current_page >= this.list.last_page || this.loading) return;
this.loading = true;
// this.getList(this.param.page + 1);
},
onShareAppMessage(e) {
let {
id: pid
} = this.userInfo
let path = `/pages/service?pid=${pid}`
this.$util.log(path)
return {
title: '',
imageUrl: '',
path,
}
},
methods: {
...mapActions(['getConfigInfo','getMineInfo', 'getUserInfo', 'updateCommonOptions', 'getServiceIndex', 'getServiceList',
'getServiceCoachList', 'getCityList', 'getClassifyList'
]),
...mapMutations(['updateUserItem', 'updateTechnicianItem', 'updateServiceItem']),
/* 宠托师入驻 */
onClickGoApply(){
console.log("用户信息",this.mineInfo)
if(this.mineInfo.coach_status == 2 || this.mineInfo.coach_status == 3) {
this.updateUserItem({
key: 'userPageType',
val:2
});
uni.navigateTo({
url:"/pages/mine"
})
} else {
uni.navigateTo({
url:'/technician/pages/apply'
})
}
},
/* 点击搜索 */
onSearch(e){
// this.searchValue = e.value
console.log("点击搜索", e)
},
async initIndex(refresh = false) {
// #ifdef H5
if (!refresh && this.$jweixin.isWechat()) {
await this.$jweixin.initJssdk();
this.toAppShare()
}
// #endif
if (!this.configInfo.id || refresh) {
await this.getConfigInfo()
}
await this.getServiceIndex()
await this.getClassifyList()
await this.getMineInfo();
this.isLoad = true
this.isLoadBanner = true
await Promise.all([this.getList(1), this.getCouponList()])
this.$util.setNavigationBarColor({
bg: this.primaryColor
})
await this.getList(1, true)
},
initRefresh() {
this.isLoadBanner = false
this.initIndex(true)
console.log("initRefresh------------------------" + this.refresh);
},
pickerChange(e, val) {
let ind = e.target.value
this.updateTechnicianItem({
key: 'cityIndex',
val: ind
})
this.updateTechnicianItem({
key: 'cityId',
val: this.cityList[ind].id
})
console.log("pickerChange------------------------" + this.refresh);
this.getList(1)
},
// 选择地区
async toChooseLocation(e) {
await this.$util.checkAuth({
type: 'userLocation'
})
let [, {
address = '',
longitude: lng,
latitude: lat,
province = '',
city = '',
district = '',
}] = await uni.chooseLocation();
if (!lng) return
let location = {
lng,
lat,
address,
province,
city,
district
}
this.updateUserItem({
key: 'location',
val: location
})
this.param.page = 1
this.getList()
},
async getList(page = 0, refresh = false, newPage={}) {
if (page) {
let param = this.$util.deepCopy(this.param)
param.page = page
this.updateTechnicianItem({
key: 'param',
val: param
})
}
let {
location
} = this
if (!location.lat) {
// #ifdef H5
if (this.$jweixin.isWechat()) {
this.$util.showLoading()
// await this.$jweixin.initJssdk();
await this.$jweixin.wxReady2();
let {
latitude: lat = 0,
longitude: lng = 0
} = await this.$jweixin.getWxLocation()
location = {
lng,
lat,
address: '定位失败',
province: '',
city: '',
district: ''
}
if (lat && lng) {
let key = `${lat},${lng}`
let data = await this.$api.base.getMapInfo({
location: key
})
let {
status,
result
} = JSON.parse(data)
if (status == 0) {
let {
address,
address_component
} = result
let {
province,
city,
district
} = address_component
location = {
lng,
lat,
address,
province,
city,
district
}
}
}
}
// #endif
// #ifndef H5
location = await this.$util.getBmapLocation()
// #endif
this.updateUserItem({
key: 'location',
val: location
})
}
let {
lng = 0,
lat = 0
} = location
let {
list: oldList,
param,
tabList,
activeIndex,
cityList,
cityIndex,
cityId: city_id,
classifyId
} = this
if (refresh) {
await this.getCityList({
lng,
lat
})
}
let {
id: type
} = tabList[activeIndex]
let ind = cityList.findIndex(item => {
return item.id == city_id
})
city_id = ind == -1 ? 0 : city_id
cityIndex = ind == -1 ? 0 : ind
this.updateTechnicianItem({
key: 'cityIndex',
val: cityIndex
})
this.updateTechnicianItem({
key: 'cityId',
val: city_id
})
this.updateServiceItem({
key: 'classifyId',
val: classifyId
})
param = Object.assign({}, param, {
lng,
lat,
type,
city_id,
classifyId
})
let {
sort,
sign
} = tabList[activeIndex]
let desc = activeIndex == 0 || sign == 1 ? '' : 'desc'
param.sort = `${sort} ${desc}`
if(newPage && JSON.stringify(newPage) !== '{}') {
uni.navigateTo({
url:`/pages/technician-list?item=${JSON.stringify(param)}&title=${newPage.title}`
})
} else {
await this.getServiceList(param)
await this.getServiceCoachList(param)
}
this.loading = false
this.$util.hideAll()
},
toAppShare() {
let {
id: pid
} = this.userInfo
// let title = '首页'
let page_url = window.location.href
if (page_url.includes('?pid=')) {
page_url = page_url.split('?pid=')[0]
}
let href = `${page_url}?pid=${pid}`
let imageUrl = ''
this.$jweixin.wxReady(() => {
this.$jweixin.showOptionMenu()
this.$jweixin.shareAppMessage(title, '', href, imageUrl)
this.$jweixin.shareTimelineMessage(title, href, imageUrl)
})
},
async userGetCoupon() {
console.log("=====userGetCoupon")
let ids = []
this.couponList.forEach(v => {
ids.push(v.id)
})
let res = await this.$api.service.userGetCoupon({
coupon_id: ids
})
this.$util.showToast({
title: `领取成功`
})
setTimeout(() => {
this.$util.goUrl({
url: '/user/pages/coupon/list'
})
}, 1000)
this.$refs.coupon_item.close()
this.loading = false
this.$util.hideAll()
},
async getCouponList() {
let list = await this.$api.service.couponList()
this.couponList = list
if (list.length > 0 && this.isLoad) {
this.$refs.coupon_item.open()
}
this.loading = false
this.$util.hideAll()
},
handerTabChange(index) {
this.updateServiceItem({
key: 'activeIndex',
val: index
})
let tabList = this.$util.deepCopy(this.tabList)
let {
is_sign,
sign,
} = tabList[index];
if (is_sign) {
tabList[index].sign = sign == 0 ? 1 : 0;
}
this.updateServiceItem({
key: 'tabList',
val: tabList
})
this.$util.showLoading()
uni.pageScrollTo({
scrollTop: 0
})
console.log("handerTabChange------------------------" + this.refresh);
this.getList(1)
},
// 详情
goDetail(index) {
let {
id
} = this.list.data[index]
let url = `/user/pages/detail?id=${id}`
this.$util.goUrl({
url
})
},
goto(url) {
this.$util.goUrl({
url
})
},
aaaaa(item) {
// this.chackIndex = index;
// let classifyId = index
this.updateServiceItem({
key: 'classifyId',
val: item.id
})
this.getList(1, false, item)
// let {
// index
// } = e.detail
// this.list[index].badge && this.list[index].badge++
// uni.showToast({
// title: `点击第${e}个宫格`,
// icon: 'none'
// })
},
}
}
</script>
<style lang="scss">
/* 2023/06/06 */
.index-swiper-container {
margin: 0 0 20rpx;
.index-swiper {
height: 661rpx;
}
.index-swiper-item {
height: 100%;
}
.index-swiper-item-img {
width: 100%;
height: 100%;
}
}
.index-menu-container {
margin:0 30rpx 30rpx;
height: 240rpx;
display: grid;
grid-template-columns: 60% calc(40% - 20rpx) repeat(3, 1fr);
grid-template-rows: repeat(2, 120rpx) repeat(3, 1fr);
grid-column-gap: 20rpx;
grid-row-gap: 20rpx;
.index-menu-item {
display: flex;
flex-direction: column;
box-sizing: border-box;
align-items: center;
justify-content: center;
border-radius: 16rpx;
letter-spacing: 4rpx;
background-color: var(--menu-bg);
.title {
font-size: 36rpx;
font-weight: bold;
color: #fff;
}
.text {
font-size: 28rpx;
color: #eee;
margin: 0;
}
.icon {
font-size: 70rpx;
color: #fff;
}
}
.one { grid-area: 1 / 1 / 3 / 2; }
.two { grid-area: 1 / 2 / 2 / 3; }
.three { grid-area: 2 / 2 / 3 / 3; }
}
.index-grid-container {
margin: 30rpx;
display: flex;
flex-wrap: wrap;
.index-grid-item {
flex-shrink: 0;
width: 25%;
margin-top: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.index-grid-item-icon {
width: 96rpx;
height: 96rpx;
}
.index-grid-item-text {
font-size: 26rpx;
color: #666;
margin-top: 8rpx;
}
}
.index-apply-container {
margin: 0 auto 10rpx;
width: calc(100% - 60rpx);
// height: 150rpx;
}
.service-search-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 26rpx 0;
box-sizing: border-box;
.uni-searchbar {
padding: 0;
}
.service-search-left {
flex-shrink: 0;
margin-right: 20rpx;
display: flex;
align-items: center;
overflow: hidden;
.service-search-city {
max-width:6em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #fff;
}
}
.service-search-right {
flex:1;
}
}
.but_class1 {
// background: url('../static/leftBut.jpg');
width: 100%;
height: 85px;
-webkit-background-size: 100%;
background-size: 100%;
border-radius: 10rpx;
}
.but_class2 {
// background: url('../static/rightBut.jpg');
width: 100%;
height: 85px;
border-radius: 10rpx;
-webkit-background-size: 100%;
background-size: 100%;
}
uni-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.images {
width: 90rpx;
height: 90rpx;
padding: 10rpx;
box-sizing: border-box;
border-radius: 50%;
background-color: rgba(109,229,243, .1);
}
.text {
font-size: 26rpx;
color: #444;
margin-top: 5px;
}
.grid-item-box-row {
flex: 1;
// position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
justify-content: center;
padding: 15px 0;
}
.butImg {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.grid-dynamic-box {
margin-bottom: 15px;
}
.grid-item-box {
flex: 1;
// position: relative;
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: column;
align-items: center;
justify-content: center;
// padding: 15px 0;
}
.pages-home {
padding-bottom: 100rpx;
.search-box {
width: 710rpx;
bottom: 0;
z-index: 9;
overflow: hidden;
}
.list-item {
.cover {
width: 180rpx;
height: 180rpx;
}
.time-long {
min-width: 72rpx;
height: 30rpx;
padding: 0 5rpx;
background: linear-gradient(270deg, #4C545A 0%, #282B34 100%);
border-radius: 4rpx;
font-size: 20rpx;
color: #FFEEB9;
margin-right: 16rpx;
}
.f-icontext {
font-size: 18rpx;
}
.text-delete {
font-size: 20rpx;
color: #B9B9B9;
}
.item-btn {
width: 130rpx;
height: 52rpx;
border-radius: 8rpx;
}
}
}
.coupon-popup {
width: 658rpx;
height: 865rpx;
position: relative;
.bg-img {
width: 100%;
height: 100%;
}
.icon-close {
font-size: 60rpx;
position: absolute;
top: 50rpx;
right: 60rpx;
z-index: 999;
}
.coupon-info {
position: absolute;
width: 100%;
height: 100%;
bottom: 0;
left: 0;
.tops {
width: 480rpx;
color: #FB4523;
position: absolute;
top: 260rpx;
>view:nth-child(1) {
font-weight: bold;
font-size: 30rpx;
}
}
.lists {
width: 500rpx;
height: 300rpx;
padding: 10rpx;
overflow-x: hidden;
position: absolute;
bottom: 222rpx;
.list {
width: 420rpx;
height: 130rpx;
margin-bottom: 10rpx;
margin-top: 5rpx;
position: relative;
>image {
width: 100%;
height: 100%;
}
>view {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 8rpx;
>view:nth-child(1) {
width: 38%;
}
>view:nth-child(2) {
display: flex;
justify-content: center;
flex: 1;
padding: 0 15rpx;
box-sizing: border-box;
}
.price {
font-size: 30rpx;
color: #FB4523;
}
.title {
font-size: 30rpx;
line-height: 36rpx;
font-weight: bold;
}
.price_text {
color: #ccc;
}
}
}
}
}
view.btns {
width: 100%;
position: absolute;
height: 82rpx;
bottom: 18rpx;
left: 0;
>view {
width: 422rpx;
height: 82rpx;
border-radius: 40rpx;
font-size: 34rpx;
color: #FFFFFF;
}
}
}
</style>