consumer-app/pages/service/service.vue

938 lines
25 KiB
Vue
Raw Normal View History

2025-12-19 12:27:55 +00:00
<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">
2026-01-13 04:12:48 +00:00
<!-- 只有店铺类型美食维修才显示距离选择器 -->
<view class="location-selector" v-if="isStoreCategory()" @click="handleLocationSelect">
2025-12-19 12:27:55 +00:00
<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"
2026-01-13 04:12:48 +00:00
@click="handleServiceItemClick(item)"
2025-12-19 12:27:55 +00:00
>
<!-- 左侧图片 -->
<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">
2026-01-13 04:12:48 +00:00
<text class="service-title">{{ item.name || item.title }}</text>
<!-- 只有店铺类型才显示距离 -->
<view class="distance-info" v-if="isStoreCategory()">
2025-12-19 12:27:55 +00:00
<image
class="location-icon"
src="/static/service/location-icon.png"
mode="aspectFill"
></image>
<text class="distance-text">{{ item.distance || 0 }}km</text>
</view>
</view>
2026-01-13 04:12:48 +00:00
<!-- 店铺类型显示门店信息 -->
<template v-if="isStoreCategory()">
<view class="service-detail">
<text class="detail-label">门店地址:</text>
<text class="detail-value">{{ item.address }}</text>
</view>
2025-12-19 12:27:55 +00:00
2026-01-13 04:12:48 +00:00
<view class="service-detail">
<text class="detail-label">联系电话:</text>
<text class="detail-value">{{ item.phone }}</text>
</view>
</template>
<!-- 消息类型显示摘要 -->
<template v-else>
<view class="service-detail" v-if="item.summary">
<text class="detail-value">{{ item.summary }}</text>
</view>
</template>
2025-12-19 12:27:55 +00:00
<view class="service-tags-row">
<view class="service-tags">
2026-01-13 04:12:48 +00:00
<view class="tag-item tag-pink"> {{ item.publishUserName || '会员特惠' }} </view>
2025-12-19 12:27:55 +00:00
<view class="tag-item tag-orange">{{
currentCategoryLabel
}}</view>
</view>
</view>
</view>
2026-01-13 04:12:48 +00:00
<!-- 只有店铺类型才显示"进店"按钮 -->
<view class="enter-btn" v-if="isStoreCategory()" @click.stop="handleEnterStore(item)">
2025-12-19 12:27:55 +00:00
进店
</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>
2026-01-13 04:12:48 +00:00
import { getDictDataByType, getGuildStorePage,getMessagePage } from "@/api/service";
2025-12-19 12:27:55 +00:00
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(); // 获取服务分类
2026-01-13 04:12:48 +00:00
// this.checkAndRequestLocation(); // 检查并请求位置权限
2025-12-19 12:27:55 +00:00
},
onShow() {
2026-01-13 04:12:48 +00:00
// 每次显示页面时刷新数据
2025-12-19 12:27:55 +00:00
// 如果分类列表已经加载,检查是否有需要高亮的分类
if (this.categoryList && this.categoryList.length > 0) {
this.checkAndSetCategory();
2026-01-13 04:12:48 +00:00
// 如果当前有选中的分类,刷新服务列表
if (this.currentCategory !== null) {
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
}
} else {
// 如果分类列表还没加载,先加载分类(分类加载完成后会自动调用 checkAndSetCategory
this.getServiceCategoryFun();
2025-12-19 12:27:55 +00:00
}
},
methods: {
// 获取系统信息
getSystemInfo() {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
},
// 获取服务分类
getServiceCategoryFun() {
getDictDataByType({ type: "member_labor_service_type" }).then((res) => {
if (res) {
2026-01-13 04:12:48 +00:00
const dictList = res || [];
// 固定添加"美食"和"维修"两个分类到列表前面
const fixedCategories = [
{ id: 'food', label: '美食', value: 'food', isStore: true }, // isStore: true 表示使用店铺接口
{ id: 'repair', label: '维修', value: 'repair', isStore: true }
];
// 将固定分类添加到前面,其他分类添加到后面
this.categoryList = [...fixedCategories, ...dictList];
2025-12-19 12:27:55 +00:00
// 分类列表加载完成后,检查是否有需要高亮的分类
this.checkAndSetCategory();
2026-01-13 04:12:48 +00:00
// 如果当前有选中的分类,加载服务列表
if (this.currentCategory !== null) {
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
}
2025-12-19 12:27:55 +00:00
}
});
},
2026-01-13 04:12:48 +00:00
// 检查并设置需要高亮的分类(不触发数据刷新,由 onShow 统一处理)
2025-12-19 12:27:55 +00:00
checkAndSetCategory() {
const app = getApp();
if (app && app.globalData && app.globalData.serviceCategory) {
const categoryValue = app.globalData.serviceCategory;
// 清除 globalData 中的分类信息
app.globalData.serviceCategory = null;
2026-01-13 04:12:48 +00:00
// 处理首页跳转过来的分类值映射('0' -> 'food', '1' -> 'repair'
let mappedValue = categoryValue;
if (categoryValue === '0') {
mappedValue = 'food';
} else if (categoryValue === '1') {
mappedValue = 'repair';
}
2025-12-19 12:27:55 +00:00
// 在分类列表中查找匹配的分类(通过 value 匹配)
const matchedCategory = this.categoryList.find(
2026-01-13 04:12:48 +00:00
item => item.value === mappedValue || item.value === categoryValue
2025-12-19 12:27:55 +00:00
);
if (matchedCategory) {
// 设置当前分类并高亮
this.currentCategory = matchedCategory.value;
this.currentCategoryLabel = matchedCategory.label;
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;
}
},
// 搜索
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;
2026-01-13 04:12:48 +00:00
// 如果切换到消息类型,清除距离筛选(消息类型不支持距离筛选)
if (!this.isStoreCategory()) {
this.selectedDistance = null;
}
2025-12-19 12:27:55 +00:00
// 重置分页,重新加载
this.pageNo = 1;
this.serviceList = [];
this.hasMore = true;
this.loadServiceList();
},
2026-01-13 04:12:48 +00:00
// 判断当前分类是否是店铺类型(美食、维修)
isStoreCategory() {
return this.currentCategory === 'food' || this.currentCategory === 'repair';
},
2025-12-19 12:27:55 +00:00
// 进店按钮点击
handleServiceItemClick(item) {
2026-01-13 04:12:48 +00:00
// 判断是店铺类型还是消息类型
if (this.isStoreCategory()) {
// 美食和维修跳转到店铺详情页面
uni.navigateTo({
url: `/pages/detail/serviceDetail?id=${item.id}&categoryLabel=${this.currentCategoryLabel}`,
});
} else {
// 其他分类跳转到富文本详情页面
const title = '详情';
const content = item.content || '';
if (content) {
uni.navigateTo({
url: `/pages/detail/richTextDetail?title=${encodeURIComponent(title)}&content=${encodeURIComponent(content)}`
});
} else {
uni.showToast({
title: '暂无内容',
icon: 'none'
});
}
}
2025-12-19 12:27:55 +00:00
},
// 进店按钮点击 - 跳转到地图详情页
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;
}
2026-01-13 04:12:48 +00:00
let res;
// 判断是店铺类型还是消息类型
if (this.isStoreCategory()) {
// 美食和维修使用店铺接口
const params = {
pageNo: this.pageNo,
pageSize: this.pageSize,
type: this.currentCategory === 'food' ? '0' : '1', // 美食: '0', 维修: '1'
name: this.searchKeyword,
};
// 如果选择了距离,添加 distance 参数
if (this.selectedDistance !== null) {
params.distance = this.selectedDistance;
}
2025-12-19 12:27:55 +00:00
2026-01-13 04:12:48 +00:00
res = await getGuildStorePage(params);
} else {
// 其他分类使用消息接口
const params = {
pageNo: this.pageNo,
pageSize: this.pageSize,
messageType: this.currentCategory, // 使用分类的 value 作为 messageType
title: this.searchKeyword, // 消息接口可能使用 title 作为搜索字段
};
res = await getMessagePage(params);
}
2025-12-19 12:27:55 +00:00
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>