结构:TINKPHP框架+公众号H5;系统开源,方便二次开发
编译:前端使用UNIAPP开发,可快速编译成APP及微信小程序和公众号H5组件:ICONFONT-UI,基于阿里图库团队UI库,用户体验棒
终端:前后端分离开发模式,开发更清晰分工更明确、提升开发效率、前端使用UNIAPP可快速编译各类终端
<template>
<view class="pages-home" v-if="isLoad">
<!-- #ifndef H5 -->
<uni-nav-bar :fixed="true" :shadow="false" :statusBar="true" title="首页" color="#ffffff"
:backgroundColor="primaryColor">
</uni-nav-bar>
<view :style="{height:`${configInfo.navBarHeight}px`}"></view>
<!-- #endif -->
<view :class="[{'rel':banner.length >0}]" :style="{height:banner.length > 0?`484rpx`:`84rpx`}">
<banner @change="goBanner" :list="banner" :margin="0" :autoplay="true" :indicatorActiveColor="primaryColor"
:dotWidth="20" :dotBottom="30" v-if="banner.length > 0">
</banner>
<view class="search-box flex-center fill-base"
:class="[{'mt-md':banner.length ==0},{'abs':banner.length>0}]" :style="{ color: primaryColor }">
<view style="width: 100%;">
<view class="servetip pl-lg pr-lg pt-sm pb-sm">
<view @tap.stop="toJump('servefc', index)" v-for="(item, index) in servefc"
:key="index" class="servefc">
<i class="iconfont" :class="item.icon"></i>
<span class="ml-sm">{{ item.text }}</span>
</view>
</view>
</view>
</view>
</view>
<!-- 分类 -->
<view class="fill-base pl-md pr-md" style="overflow: hidden;" v-if="service_cate.length>0">
<column @change="goCate" :list="service_cate" :indicatorActiveColor="primaryColor" :colNum="5" :rowNum="2">
</column>
</view>
<view class="fill-base pl-lg pr-lg pb-lg" v-if="recommend_list && recommend_list.length > 0">
<view @tap.stop="$util.goUrl({url: `/pages/technician`,openType: `reLaunch`})" class="flex-between pb-lg">
<view class="f-st-title text-bold flex-between">
<view class="mr-md" style="width: 15px;height: 17px;">
<image src="/static/img/icon_technician.png" class="van-img" style="object-fit: cover;"></image>
</view>
{{`推荐${$t('action.attendantName')}`}}
</view>
<view class="flex-y-center f-caption c-caption">查看更多<i class="iconfont icon-right"
style="font-size: 24rpx;"></i></view>
</view>
<scroll-view scroll-x class="recommend-technician">
<block v-for="(item,index) in recommend_list" :key="index">
<view @tap.stop="toTechnician(index)" class="recommend-item type-1" v-if="recommend_style == 1">
<!-- #ifdef H5 -->
<view class="cover radius-16">
<view class="h5-image cover radius-16"
:style="{ backgroundImage : `url('${item.work_img}')`}">
</view>
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<image mode="aspectFill" lazy-load class="cover radius-16" :src="item.work_img"></image>
<!-- #endif -->
<view class="flex-center f-desc c-title text-bold mt-md">
<view class="ellipsis">{{item.coach_name}}</view>
</view>
<view class="flex-center">
<view class="new-technician flex-center f-icontext radius"
:style="{height:`33rpx`,width:`80rpx`,color:primaryColor,border:`1rpx solid ${primaryColor}`}" v-if="item.is_new">新人
</view>
<view class="f-icontext c-caption" v-else>30天接单{{item.order_count||0}}
</view>
</view>
</view>
<view @tap.stop="toTechnician(index)" class="recommend-item type-2 pd-md"
v-if="recommend_style == 2">
<view class="flex-center pb-sm">
<!-- #ifdef H5 -->
<view class="cover radius">
<view class="h5-image cover radius"
:style="{ backgroundImage : `url('${item.work_img}')`}">
</view>
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<image mode="aspectFill" lazy-load class="cover radius" :src="item.work_img"></image>
<!-- #endif -->
<view class="flex-1 ml-sm">
<view class="f-desc ellipsis">{{item.coach_name}}</view>
<view class="flex-y-baseline" style="margin-top: 4rpx;">
<i class="iconfont iconyduixingxingshixin icon-font-color"></i>
<view class="star-text flex-y-center f-caption">{{item.star}}</view>
</view>
</view>
</view>
<view class="flex-center">
<view class="new-technician flex-center f-icontext radius"
:style="{height:`33rpx`,width:`80rpx`,color:primaryColor,border:`1rpx solid ${primaryColor}`}" v-if="item.is_new">新人
</view>
<view class="f-icontext c-caption" v-else>30天接单{{item.order_count||0}}
</view>
</view>
</view>
</block>
</scroll-view>
</view>
<view class="fill-base pl-lg pr-lg">
<view class="fill-base flex-between">
<view class="f-st-title text-bold flex-between">
<view class="mr-md" style="width: 15px;height: 17px;">
<image src="/static/img/icon_project.png" class="van-img" style="object-fit: cover;"></image>
</view>
推荐项目
</view>
</view>
</view>
<view class="fill-base pd-lg b-1px-b" v-for="(item,index) in list.data" :key="index">
<service-list-item :info="item"></service-list-item>
</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 flex-center">
<!-- #ifdef H5 -->
<view class="h5-image bg-img"
:style="{ backgroundImage : `url('https://lbqnyv2.migugu.com/bianzu3.png')`}">
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<image mode="aspectFill" lazy-load class="bg-img" src="https://lbqnyv2.migugu.com/bianzu3.png"></image>
<!-- #endif -->
<i @tap.stop="$refs.coupon_item.close()" class="iconfont icon-close c-base"></i>
</image>
<view class="coupon-info flex-center flex-column">
<view class="tops flex-center flex-column">
<view class="">
成功领取
</view>
<view class="">
卡券将放入"我的-我的卡券"
</view>
</view>
<view class="lists flex-center">
<scroll-view scroll-y style="width: 420rpx;height:100%;">
<view class="list flex-between" v-for="(item, index) in couponList" :key="index">
<image src="https://lbqny.migugu.com/admin/anmo/coupon/coupon.png" mode="aspectFill">
</image>
<view class="flex-between">
<view class="flex-center flex-column">
<view class="price">
{{item.discount}}
</view>
<view class="price_text">
{{item.full*1>0?`满${item.full}可用`:`立减`}}
</view>
</view>
<view class="title flex-y-center">
<view class="ellipsis-3">
{{item.title}}
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
<view class="btns flex-center" @tap.stop="userGetCoupon">
<view class="flex-center">
领取到卡包
</view>
</view>
</view>
</uni-popup>
<view :style="{height: `${configInfo.tabbarHeight}px`}"></view>
<tabbar :cur="1"></tabbar>
<!-- #ifdef APP-PLUS -->
<login-info></login-info>
<!-- #endif -->
</view>
</template>
<script>
import {
mapState,
mapActions,
mapMutations
} from "vuex"
import siteInfo from '@/siteinfo.js';
import serviceListItem from "@/components/service-list-item.vue"
import tabbar from "@/components/tabbar.vue"
export default {
components: {
serviceListItem,
tabbar
},
data() {
return {
couponList: [], //优惠券
isLoad: false,
options: {},
loading: true,
lockTap: false,
servefc: [{
icon: 'icon-kzj',
text: '实名认证'
}, {
icon: 'icon-kpk',
text: '爽约包赔'
}, {
icon: 'icon-sybp',
text: '超时秒退'
}, {
icon: 'icon-kzz',
text: '资质证书'
}],
}
},
computed: mapState({
pageActive: state => state.service.pageActive,
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,
service_cate: state => state.service.service_cate,
recommend_list: state => state.service.recommend_list,
recommend_style: state => state.service.recommend_style,
primaryColor: state => state.config.configInfo.primaryColor,
subColor: state => state.config.configInfo.subColor,
configInfo: state => state.config.configInfo,
autograph: state => state.user.autograph,
userInfo: state => state.user.userInfo,
location: state => state.user.location,
isGzhLogin: state => state.user.isGzhLogin,
haveShieldOper: state => state.user.haveShieldOper,
}),
async onLoad(options) {
this.$util.showLoading()
options = await this.updateCommonOptions(options)
this.options = options
uni.onNetworkStatusChange((res) => {
let {
isConnected
} = res
if (isConnected && !this.pageActive) {
this.initIndex()
return
}
})
await this.initIndex()
},
async onShow() {
// #ifdef H5
if (this.$jweixin.isWechat()) {
await this.$jweixin.initJssdk();
this.toAppShare()
}
// #endif
if (this.haveShieldOper == 2) {
this.initIndex()
this.updateUserItem({
key: 'haveShieldOper',
val: 0
})
}
if (this.pageActive && this.userInfo.id) {
this.getCouponList()
}
},
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 = 0
} = this.userInfo
let path = `/pages/service?pid=${pid}`
this.$util.log(path)
return {
title: '',
imageUrl: '',
path,
}
},
methods: {
...mapActions(['getConfigInfo', 'getUserInfo', 'updateCommonOptions', 'getServiceIndex', 'getServiceList']),
...mapMutations(['updateServiceItem', 'updateTechnicianItem', 'updateUserItem']),
async initIndex(refresh = false) {
let {
pid = 0
} = this.options
if (!refresh && this.pageActive && !pid) {
this.isLoad = true
this.loading = false
this.$util.hideAll()
return
}
let {
isGzhLogin
} = this
let {
id: uid = 0
} = this.userInfo
if (pid && !uid) {
// #ifdef H5
if (isGzhLogin) {
setTimeout(() => {
this.getUserInfo()
}, 1000)
} else {
this.getUserInfo()
}
// #endif
// #ifndef H5
await this.getUserInfo()
// #endif
}
if (!this.configInfo.id || refresh) {
await this.getConfigInfo()
}
let {
location
} = this
let {
plugAuth = {}
} = this.configInfo
let {
recommend = false
} = plugAuth
if (recommend && !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: ''
}
}
// #endif
// #ifndef H5
location = await this.$util.getBmapLocation()
// #endif
this.updateUserItem({
key: 'location',
val: location
})
}
let {
lng = 0,
lat = 0
} = location
await this.getServiceIndex({
lat,
lng
})
this.updateServiceItem({
key: 'pageActive',
val: true
})
this.isLoad = true
if (this.userInfo.id) {
await Promise.all([this.getList(1), this.getCouponList()])
} else {
await this.getList(1)
}
},
initRefresh() {
this.initIndex(true)
},
toAppShare() {
let {
id: pid = 0
} = this.userInfo
let title = '首页'
let {
siteroot
} = siteInfo
let url = siteroot.split('/index.php')[0]
let href = `${url}/h5/#/pages/service?pid=${pid}`
let imageUrl = ''
this.$jweixin.wxReady(() => {
this.$jweixin.showOptionMenu()
this.$jweixin.shareAppMessage(title, '', href, imageUrl)
this.$jweixin.shareTimelineMessage(title, href, imageUrl)
})
},
// 轮播图/广告图跳转
goBanner(e) {
// connect_type 1查看大图,2文章
let {
connect_type,
type_id: id = 0,
img: current
} = e
switch (connect_type) {
case 1:
this.$util.previewImage({
current,
urls: [current]
})
break;
case 2:
this.$util.goUrl({
url: `/user/pages/article?id=${id}`
})
break;
}
},
goCate(e) {
let {
id,
title,
url: link = ''
} = e
let url = link || `/user/pages/service/list?id=${id}&title=${title}`
this.$util.goUrl({
url
})
},
async 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()
},
async getList(page = 0) {
if (page) {
let param = this.$util.deepCopy(this.param)
param.page = page
this.updateServiceItem({
key: 'param',
val: param
})
}
let {
list: oldList,
param,
tabList,
activeIndex
} = this
let {
sort,
sign
} = tabList[activeIndex]
let desc = activeIndex == 0 || sign == 1 ? '' : 'desc'
param.sort = `${sort} ${desc}`
await this.getServiceList(param)
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
})
this.getList(1)
},
toTechnician(index) {
let {
id,
city_id,
coach_name
} = this.recommend_list[index]
this.updateTechnicianItem({
key: 'pageActive',
val: false
})
this.$util.goUrl({
url: `/pages/technician?coach_id=${id}&coach_name=${coach_name}&city_id=${city_id}`,
openType: `reLaunch`
})
}
}
}
</script>
<style lang="scss">
.pages-home {
.search-box {
width: 100%;
bottom: 0;
z-index: 9;
overflow: hidden;
}
.recommend-technician {
white-space: nowrap;
width: 690rpx;
.recommend-item {
display: inline-block;
}
.recommend-item.type-1 {
width: 180rpx;
margin-left: 26rpx;
.cover {
width: 180rpx;
height: 180rpx;
}
.ellipsis {
max-width: 180rpx;
}
}
.recommend-item.type-2 {
width: 203rpx;
height: 151rpx;
background: #F4F6F7;
border-radius: 12rpx;
margin-left: 20rpx;
.cover {
width: 70rpx;
height: 70rpx;
}
.ellipsis {
max-width: 82rpx;
}
.iconyduixingxingshixin {
font-size: 26rpx;
background-image: -webkit-linear-gradient(270deg, #FAD961 0%, #F76B1C 100%);
}
.star-text {
height: 26rpx;
color: #FF9519;
margin-left: 6rpx;
}
.new-technician {
width: 67rpx;
height: 30rpx;
border-radius: 8rpx;
transform: rotateZ(360deg);
}
}
.recommend-item:nth-child(1) {
margin-left: 0;
}
}
.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: 24rpx;
color: #B9B9B9;
}
.item-btn {
width: 150rpx;
height: 52rpx;
border-radius: 100rpx;
}
}
}
.radius-top {
border-radius: 30rpx 30rpx 0 0;
}
.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: 0rpx;
left: 0;
>view {
width: 422rpx;
height: 82rpx;
border-radius: 40rpx;
font-size: 34rpx;
color: #FFFFFF;
}
}
}
.servetip {
font-size: 28rpx;
font-weight: 400;
display: flex;
justify-content: space-between;
align-items: center;
margin: 22rpx 0;
}
.servefc{
display: flex;
align-items: center;
}
.van-image {
position: relative;
display: inline-block;
}
.van-img {
width: 100%;
height: 100%;
}
.margin-20 {
margin: 0.53333rem;
}
</style>