consumer-app/pages/service/service.vue

861 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="service-page">
<!-- 顶部导航栏 -->
<view class="header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="header-content">
<view class="search-box">
<image
class="search-icon"
src="/static/service/search_icon.png"
mode="aspectFill"
></image>
<input
class="search-input"
type="text"
placeholder="请输入搜索内容"
v-model="searchKeyword"
@confirm="handleSearch"
/>
</view>
<view class="search-btn" @click="handleSearch">
<text>搜索</text>
</view>
</view>
</view>
<!-- 分类标签栏 -->
<view class="category-bar">
<view class="location-selector" @click="handleLocationSelect">
<text class="location-text">{{ selectedDistance ? `附近${selectedDistance}km` : '附近' }}</text>
<image
class="location-arrow"
src="/static/service/location-arrow.png"
mode="aspectFill"
></image>
</view>
<scroll-view
class="category-scroll"
scroll-x="true"
:show-scrollbar="false"
:scroll-left="scrollLeft"
:scroll-with-animation="true"
enable-passive
>
<view class="category-list">
<view
class="category-item"
:class="{ active: currentCategory === item.value }"
v-for="(item, index) in categoryList"
:key="item.id"
@click="handleCategoryClick(item, index)"
>
<text>{{ item.label }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 服务列表 -->
<scroll-view
class="service-list"
scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="handleLoadMore"
:lower-threshold="100"
>
<!-- 空数据提示 -->
<view class="empty-state" v-if="!loading && serviceList.length === 0">
<image
class="empty-icon"
src="/static/home/entry_icon.png"
mode="aspectFit"
></image>
<text class="empty-text">暂无服务数据</text>
<text class="empty-desc">请尝试切换分类或搜索条件</text>
</view>
<!-- 服务列表项 -->
<view
class="service-item"
v-for="(item, index) in serviceList"
:key="index"
@click="handleServiceItemClick(item)"
>
<!-- 左侧图片 -->
<view class="service-image-wrapper">
<image
class="service-image"
:src="item.coverUrl"
mode="aspectFill"
></image>
</view>
<!-- 右侧信息 -->
<view class="service-info">
<view class="service-title-row">
<text class="service-title">{{ item.name }}</text>
<view class="distance-info">
<image
class="location-icon"
src="/static/service/location-icon.png"
mode="aspectFill"
></image>
<text class="distance-text">{{ item.distance || 0 }}km</text>
</view>
</view>
<view class="service-detail">
<text class="detail-label">门店地址:</text>
<text class="detail-value">{{ item.address }}</text>
</view>
<view class="service-detail">
<text class="detail-label">联系电话:</text>
<text class="detail-value">{{ item.phone }}</text>
</view>
<view class="service-tags-row">
<view class="service-tags">
<view class="tag-item tag-pink"> 会员特惠 </view>
<view class="tag-item tag-orange">{{
currentCategoryLabel
}}</view>
</view>
</view>
</view>
<view class="enter-btn" @click.stop="handleEnterStore(item)">
进店
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more" v-if="serviceList.length > 0">
<text v-if="loadingMore" class="load-more-text">加载中...</text>
<text v-else-if="!hasMore" class="load-more-text">没有更多数据了</text>
<text v-else class="load-more-text">上拉加载更多</text>
</view>
</scroll-view>
<!-- 距离选择弹窗 -->
<view class="distance-picker-mask" v-if="showDistancePicker" @click="showDistancePicker = false">
<view class="distance-picker" @click.stop>
<view class="picker-header">
<text class="picker-title">选择距离范围</text>
<view class="picker-close" @click="showDistancePicker = false">×</view>
</view>
<view class="picker-options">
<view
class="picker-option"
:class="{ active: selectedDistance === null }"
@click="clearDistance"
>
<text>不限距离</text>
</view>
<view
class="picker-option"
:class="{ active: selectedDistance === 1 }"
@click="selectDistance(1)"
>
<text>1km</text>
</view>
<view
class="picker-option"
:class="{ active: selectedDistance === 3 }"
@click="selectDistance(3)"
>
<text>3km</text>
</view>
<view
class="picker-option"
:class="{ active: selectedDistance === 5 }"
@click="selectDistance(5)"
>
<text>5km</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import { getDictDataByType, getGuildStorePage } from "@/api/service";
export default {
data() {
return {
statusBarHeight: 0,
searchKeyword: "",
currentCategory: null, // 默认选中"维修"
currentCategoryLabel: "",
scrollLeft: 0,
categoryList: [],
serviceList: [],
// 分页相关
pageNo: 1,
pageSize: 10,
total: 0,
hasMore: true,
loading: false,
loadingMore: false,
refreshing: false,
// 位置相关
selectedDistance: null, // 选中的距离kmnull表示不限距离
showDistancePicker: false, // 是否显示距离选择器
};
},
onLoad() {
this.getSystemInfo();
this.getServiceCategoryFun(); // 获取服务分类
this.checkAndRequestLocation(); // 检查并请求位置权限
},
onShow() {
// 如果分类列表已经加载,检查是否有需要高亮的分类
if (this.categoryList && this.categoryList.length > 0) {
this.checkAndSetCategory();
}
},
methods: {
// 获取系统信息
getSystemInfo() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
},
// 获取服务分类
getServiceCategoryFun() {
getDictDataByType({ type: "member_labor_service_type" }).then((res) => {
if (res) {
this.categoryList = res || [];
// 分类列表加载完成后,检查是否有需要高亮的分类
this.checkAndSetCategory();
}
});
},
// 检查并设置需要高亮的分类
checkAndSetCategory() {
const app = getApp();
if (app && app.globalData && app.globalData.serviceCategory) {
const categoryValue = app.globalData.serviceCategory;
// 清除 globalData 中的分类信息
app.globalData.serviceCategory = null;
// 在分类列表中查找匹配的分类(通过 value 匹配)
const matchedCategory = this.categoryList.find(
item => item.value === categoryValue
);
if (matchedCategory) {
// 设置当前分类并高亮
this.currentCategory = matchedCategory.value;
this.currentCategoryLabel = matchedCategory.label;
// 重置分页,重新加载数据
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
return;
}
}
// 如果没有从 globalData 获取到分类,且当前没有选中分类,默认高亮第一个分类
if (this.currentCategory === null && this.categoryList && this.categoryList.length > 0) {
const firstCategory = this.categoryList[0];
this.currentCategory = firstCategory.value;
this.currentCategoryLabel = firstCategory.label;
// 重置分页,重新加载数据
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
}
},
// 搜索
handleSearch() {
// 重置分页,重新加载
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
},
// 检查并请求位置权限
checkAndRequestLocation() {
// 先检查是否已有存储的位置信息
const savedLocation = uni.getStorageSync("userLocation");
if (savedLocation && savedLocation.latitude && savedLocation.longitude) {
// 已有位置信息,不需要再次获取
return;
}
// 如果没有位置信息,请求授权并获取位置
uni.authorize({
scope: "scope.userLocation",
success: () => {
// 授权成功,获取位置
this.getUserLocation();
},
fail: () => {
// 授权失败,提示用户
uni.showModal({
title: "位置权限",
content: "需要获取您的位置信息以提供附近店铺服务,是否前往设置开启?",
confirmText: "去设置",
cancelText: "取消",
success: (res) => {
if (res.confirm) {
uni.openSetting({
success: (settingRes) => {
if (settingRes.authSetting["scope.userLocation"]) {
// 用户开启了位置权限,获取位置
this.getUserLocation();
}
},
});
}
},
});
},
});
},
// 获取用户当前位置
getUserLocation() {
uni.getLocation({
type: "gcj02",
success: (res) => {
const location = {
latitude: res.latitude,
longitude: res.longitude,
};
// 存储位置信息
uni.setStorageSync("userLocation", location);
},
fail: (err) => {
console.error("获取位置失败:", err);
},
});
},
// 选择位置/距离
handleLocationSelect() {
this.showDistancePicker = true;
},
// 选择距离
selectDistance(distance) {
this.selectedDistance = distance;
this.showDistancePicker = false;
// 重置分页,重新加载数据
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
},
// 清除距离筛选
clearDistance() {
this.selectedDistance = null;
this.showDistancePicker = false;
// 重置分页,重新加载数据
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
},
// 分类点击
handleCategoryClick(item) {
this.currentCategory = item.value;
this.currentCategoryLabel = item.label;
// 重置分页,重新加载
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
},
// 进店按钮点击
handleServiceItemClick(item) {
// 跳转到店铺详情页面
uni.navigateTo({
url: `/pages/detail/serviceDetail?id=${item.id}&categoryLabel=${this.currentCategoryLabel}`,
});
},
// 进店按钮点击 - 跳转到地图详情页
handleEnterStore(item) {
uni.navigateTo({
url: `/pages/detail/mapDetail?id=${item.id}`,
});
},
// 加载服务列表
async loadServiceList(isLoadMore = false) {
// 如果正在加载或没有更多数据,则不加载
if (this.loading || this.loadingMore || (!isLoadMore && !this.hasMore)) {
return;
}
try {
if (isLoadMore) {
this.loadingMore = true;
} else {
this.loading = true;
}
// 构建请求参数
const params = {
pageNo: this.pageNo,
pageSize: this.pageSize,
type: this.currentCategory,
name: this.searchKeyword,
};
// 如果选择了距离,添加 distance 参数
if (this.selectedDistance !== null) {
params.distance = this.selectedDistance;
}
const res = await getGuildStorePage(params);
if (res) {
const newList = res.list || [];
this.total = res.total || 0;
if (isLoadMore) {
// 加载更多,追加数据
this.serviceList = [...this.serviceList, ...newList];
} else {
// 首次加载或刷新,替换数据
this.serviceList = newList;
}
// 判断是否还有更多数据
this.hasMore = this.serviceList.length < this.total;
}
} catch (error) {
console.error("加载服务列表失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
this.loading = false;
this.loadingMore = false;
this.refreshing = false;
}
},
// 下拉刷新
handleRefresh() {
this.refreshing = true;
this.pageNo = 1;
this.hasMore = true;
this.loadServiceList(false);
},
// 上拉加载更多
handleLoadMore() {
if (this.hasMore && !this.loadingMore && !this.loading) {
this.pageNo += 1;
this.loadServiceList(true);
}
},
},
};
</script>
<style lang="scss" scoped>
.service-page {
height: 100vh;
background-color: #e2e8f1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 顶部导航栏 */
.header {
margin-left: 22rpx;
height: 88rpx;
display: flex;
align-items: center;
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 488rpx;
height: 60rpx;
background: #ffffff;
border-radius: 58rpx;
box-sizing: border-box;
padding: 13rpx 9rpx 13rpx 18rpx;
.search-box {
display: flex;
align-items: center;
.search-icon {
width: 34rpx;
height: 34rpx;
margin-right: 10rpx;
}
.search-input {
height: 100%;
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 24rpx;
color: #888888;
}
}
.search-btn {
width: 120rpx;
height: 47rpx;
background: #004294;
border-radius: 24rpx 24rpx 24rpx 24rpx;
display: flex;
align-items: center;
justify-content: center;
text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 24rpx;
color: #ffffff;
}
}
}
}
/* 分类标签栏 */
.category-bar {
display: flex;
align-items: center;
padding: 24rpx 0;
.location-selector {
display: flex;
align-items: center;
padding: 0 24rpx 0 46rpx;
flex-shrink: 0;
.location-text {
margin-right: 8rpx;
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 26rpx;
color: #494949;
}
.location-arrow {
width: 18rpx;
height: 12rpx;
}
}
.category-scroll {
flex: 1;
width: 0; // 重要flex布局时需要设置width: 0才能正确计算宽度
height: 60rpx;
overflow: hidden;
.category-list {
display: flex;
align-items: center;
height: 100%;
white-space: nowrap;
padding-right: 30rpx; // 最后一个元素右边距
.category-item {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 33rpx;
height: 60rpx;
position: relative;
flex-shrink: 0; // 重要:防止被压缩
white-space: nowrap;
text {
font-size: 28rpx;
color: #999999;
font-weight: 400;
white-space: nowrap;
}
&.active {
text {
color: #004294;
font-weight: 600;
}
// &::after {
// content: "";
// position: absolute;
// bottom: 0;
// left: 24rpx;
// right: 24rpx;
// height: 4rpx;
// background-color: #004294;
// border-radius: 2rpx;
// }
}
}
}
}
}
/* 服务列表 */
.service-list {
width: 95%;
margin: 0 auto;
flex: 1;
height: 0; // 配合 flex: 1 使用,让 scroll-view 可以滚动
/* 空数据提示 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
min-height: 500rpx;
.empty-icon {
width: 356rpx;
height: 266rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 28rpx;
color: #999999;
margin-bottom: 16rpx;
}
.empty-desc {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 24rpx;
color: #cccccc;
}
}
/* 加载更多提示 */
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
min-height: 80rpx;
.load-more-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
}
.service-item {
display: flex;
background-color: #ffffff;
border-radius: 20rpx;
margin-bottom: 21rpx;
padding: 25rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
position: relative;
.service-image-wrapper {
width: 186rpx;
height: 200rpx;
margin-right: 24rpx;
.service-image {
width: 100%;
height: 100%;
}
}
.service-info {
flex: 1;
display: flex;
flex-direction: column;
.service-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
.service-title {
flex: 1;
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 30rpx;
color: #1a1819;
}
.distance-info {
display: flex;
align-items: center;
.location-icon {
width: 17rpx;
height: 20rpx;
margin-right: 5rpx;
}
.distance-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 22rpx;
color: #004294;
}
}
}
.service-detail {
display: flex;
align-items: flex-start;
margin-bottom: 12rpx;
line-height: 1.5;
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 22rpx;
color: #888888;
.detail-label {
margin-right: 8rpx;
flex-shrink: 0;
}
.detail-value {
flex: 1;
}
}
.service-tags-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: auto;
.service-tags {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
.tag-item {
padding: 7rpx 12rpx;
font-size: 20rpx;
&.tag-pink {
background-color: rgba(213, 28, 60, 0.1);
color: #d51c3c;
}
&.tag-orange {
background-color: rgba(255, 107, 0, 0.1);
color: #ff6b00;
}
}
}
}
}
.enter-btn {
position: absolute;
right: 21rpx;
bottom: 25rpx;
padding: 12rpx 40rpx;
background: #004294;
border-radius: 25rpx 25rpx 25rpx 25rpx;
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 26rpx;
color: #ffffff;
}
}
}
/* 距离选择器 */
.distance-picker-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
}
.distance-picker {
width: 100%;
background-color: #ffffff;
border-radius: 24rpx 24rpx 0 0;
padding: 0 0 40rpx 0;
max-height: 60vh;
.picker-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 30rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
.picker-title {
font-family: PingFang-SC, PingFang-SC;
font-weight: bold;
font-size: 32rpx;
color: #1a1819;
}
.picker-close {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 48rpx;
color: #999999;
line-height: 1;
}
}
.picker-options {
padding: 20rpx 0;
.picker-option {
padding: 30rpx;
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 28rpx;
color: #333333;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&.active {
color: #004294;
background-color: rgba(0, 66, 148, 0.05);
}
}
}
}
</style>