新增统一导航栏组件YjyfNavBar,替换各页面原生导航 新增服务选择抽屉组件GoodsPickerDrawer,实现分类选品流程 新增tabbar图标矢量资源,优化底部导航样式 新增后端运力统计接口与多商品下单逻辑 修复页面跳转逻辑,统一使用switchTab/redirectTo 优化订单创建与评价页的UI组件化,提升一致性
304 行
9.5 KiB
Vue
304 行
9.5 KiB
Vue
<template>
|
||
<view class="drawer-mask" v-if="visible" @tap="close">
|
||
<view class="drawer-sheet" @tap.stop>
|
||
<!-- 抽屉头部 -->
|
||
<view class="drawer-head">
|
||
<text class="dh-title">选择{{ currentCatName ? (currentCatName + '服务') : '商品服务' }}</text>
|
||
<view class="dh-close clickable-hover" @tap="close">
|
||
<image class="dh-close-img" src="/static/icons/close.svg" mode="aspectFit" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 搜索栏(纯矢量放大镜图标) -->
|
||
<view class="drawer-search">
|
||
<view class="ds-box">
|
||
<image class="ds-icon-img" src="/static/icons/search-gray.svg" mode="aspectFit" />
|
||
<input
|
||
class="ds-input"
|
||
v-model="keyword"
|
||
:placeholder="currentCatName ? ('搜索' + currentCatName + '服务项目') : '搜索服务商品名称'"
|
||
confirm-type="search"
|
||
@confirm="onSearchConfirm"
|
||
@input="onSearchInput"
|
||
/>
|
||
<view class="ds-clear" v-if="keyword" @tap="clearSearch">
|
||
<image class="ds-clear-img" src="/static/icons/close.svg" mode="aspectFit" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 左右两栏联动:左侧一级真实类目,右侧真实服务商品列表 -->
|
||
<view class="drawer-body">
|
||
<!-- 左侧真实类目列表 -->
|
||
<scroll-view class="left-col" scroll-y="true">
|
||
<view
|
||
v-for="cat in catList"
|
||
:key="cat.id"
|
||
class="cat-item"
|
||
:class="{ on: activeCatId === cat.id }"
|
||
@tap="selectCategory(cat)"
|
||
>
|
||
<text class="cat-name">{{ cat.name }}</text>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 右侧真实服务项目列表 -->
|
||
<scroll-view class="right-col" scroll-y="true">
|
||
<view class="loading-state" v-if="loading">
|
||
<text class="loading-txt">正在加载服务项目...</text>
|
||
</view>
|
||
|
||
<view class="service-list" v-else-if="serviceList.length > 0">
|
||
<view
|
||
class="service-card clickable-hover"
|
||
v-for="item in serviceList"
|
||
:key="item.id"
|
||
@tap="onSelect(item)"
|
||
>
|
||
<view class="sc-main">
|
||
<text class="sc-name">{{ item.name }}</text>
|
||
<text class="sc-desc" v-if="item.description">{{ item.description }}</text>
|
||
</view>
|
||
<view class="sc-price-box">
|
||
<text class="sc-price-num" v-if="item.price_type === 'fixed'">¥{{ item.base_price }}</text>
|
||
<text class="sc-price-tag" v-else-if="item.price_type === 'quote'">师傅报价</text>
|
||
<text class="sc-price-num" v-else>¥{{ item.base_price }}起</text>
|
||
<text class="sc-price-unit" v-if="item.unit">/{{ item.unit }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="empty-tip" v-else>
|
||
<text class="empty-txt">当前分类下暂无上架服务</text>
|
||
</view>
|
||
|
||
<!-- 底部找不到商品引导(真实矢量橙色小箭头) -->
|
||
<view class="bottom-helper clickable-hover" @tap="onCustomFallback">
|
||
<text class="bh-txt">找不到您需要的型号?</text>
|
||
<text class="bh-link">点这里直接发单</text>
|
||
<image class="bh-arrow-img" src="/static/icons/arrow-right-orange.svg" mode="aspectFit" />
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch } from 'vue';
|
||
import api from '@/utils/api.js';
|
||
|
||
const props = defineProps({
|
||
visible: { type: Boolean, default: false },
|
||
catId: { type: [Number, String], default: '' },
|
||
catName: { type: String, default: '' }
|
||
});
|
||
|
||
const emit = defineEmits(['update:visible', 'select', 'custom']);
|
||
|
||
const keyword = ref('');
|
||
const catList = ref([]);
|
||
const activeCatId = ref(0);
|
||
const serviceList = ref([]);
|
||
const loading = ref(false);
|
||
|
||
let searchTimer = null;
|
||
|
||
const currentCatName = computed(() => {
|
||
const hit = catList.value.find(c => c.id === activeCatId.value);
|
||
if (hit) return hit.name;
|
||
return props.catName || '';
|
||
});
|
||
|
||
// 获取真实服务类目(来自后端接口)
|
||
async function loadCategories() {
|
||
try {
|
||
const res = await api.cats();
|
||
if (res && res.list && res.list.length > 0) {
|
||
catList.value = res.list;
|
||
return res.list;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
return [];
|
||
}
|
||
|
||
// 获取具体分类下的真实服务列表(来自后端接口)
|
||
async function loadServices(catId, kw = '') {
|
||
loading.value = true;
|
||
try {
|
||
const params = {};
|
||
if (catId > 0) params.cat_id = catId;
|
||
if (kw) params.keyword = kw;
|
||
const res = await api.services(params);
|
||
if (res && res.list) {
|
||
serviceList.value = res.list;
|
||
} else {
|
||
serviceList.value = [];
|
||
}
|
||
} catch (e) {
|
||
serviceList.value = [];
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
function selectCategory(cat) {
|
||
activeCatId.value = cat.id;
|
||
keyword.value = '';
|
||
loadServices(cat.id);
|
||
}
|
||
|
||
function onSearchInput() {
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(() => {
|
||
loadServices(activeCatId.value, keyword.value.trim());
|
||
}, 300);
|
||
}
|
||
|
||
function onSearchConfirm() {
|
||
clearTimeout(searchTimer);
|
||
loadServices(activeCatId.value, keyword.value.trim());
|
||
}
|
||
|
||
function clearSearch() {
|
||
keyword.value = '';
|
||
loadServices(activeCatId.value);
|
||
}
|
||
|
||
// 监听抽屉打开与外部传入的 catId 变动
|
||
watch(
|
||
[() => props.visible, () => props.catId],
|
||
async ([newVisible, newCatId]) => {
|
||
if (!newVisible) return;
|
||
keyword.value = '';
|
||
|
||
let cats = catList.value;
|
||
if (!cats.length) {
|
||
cats = await loadCategories();
|
||
}
|
||
|
||
let targetId = Number(newCatId);
|
||
if (!targetId || !cats.some(c => c.id === targetId)) {
|
||
// 若未传入有效 catId,优先匹配名称,否则默认选中第 1 个真实分类
|
||
if (props.catName) {
|
||
const hit = cats.find(c => c.name === props.catName || props.catName.includes(c.name));
|
||
if (hit) targetId = hit.id;
|
||
}
|
||
if (!targetId && cats.length > 0) {
|
||
targetId = cats[0].id;
|
||
}
|
||
}
|
||
|
||
activeCatId.value = targetId;
|
||
loadServices(targetId);
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
|
||
function close() {
|
||
emit('update:visible', false);
|
||
}
|
||
|
||
function onSelect(item) {
|
||
const hitCat = catList.value.find(c => c.id === activeCatId.value);
|
||
emit('select', {
|
||
service_id: item.id,
|
||
item_name: item.name,
|
||
spec_name: item.name,
|
||
cat_id: item.cat_id || activeCatId.value,
|
||
cat_name: hitCat ? hitCat.name : (props.catName || ''),
|
||
base_price: Number(item.base_price) || 0,
|
||
unit: item.unit || '件',
|
||
price_type: item.price_type || 'fixed',
|
||
description: item.description || ''
|
||
});
|
||
close();
|
||
}
|
||
|
||
function onCustomFallback() {
|
||
const hitCat = catList.value.find(c => c.id === activeCatId.value);
|
||
emit('custom', {
|
||
cat_id: activeCatId.value,
|
||
cat_name: hitCat ? hitCat.name : ''
|
||
});
|
||
close();
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.drawer-mask {
|
||
position: fixed; inset: 0; background: rgba(0,0,0,0.55); z-index: 999;
|
||
display: flex; flex-direction: column; justify-content: flex-end;
|
||
}
|
||
.drawer-sheet {
|
||
background: #fff; border-radius: 28rpx 28rpx 0 0; height: 75vh;
|
||
display: flex; flex-direction: column; overflow: hidden;
|
||
}
|
||
.drawer-head {
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
padding: 30rpx 32rpx 20rpx;
|
||
}
|
||
.dh-title { font-size: 32rpx; font-weight: bold; color: #222; }
|
||
.dh-close {
|
||
width: 56rpx; height: 56rpx; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.dh-close-img { width: 32rpx; height: 32rpx; }
|
||
|
||
.drawer-search { padding: 0 32rpx 20rpx; }
|
||
.ds-box {
|
||
background: #F4F5F7; border-radius: 12rpx; height: 72rpx;
|
||
display: flex; align-items: center; padding: 0 20rpx;
|
||
}
|
||
.ds-icon-img { width: 30rpx; height: 30rpx; margin-right: 12rpx; opacity: 0.6; }
|
||
.ds-input { flex: 1; font-size: 26rpx; color: #333; }
|
||
.ds-clear {
|
||
width: 44rpx; height: 44rpx; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.ds-clear-img { width: 24rpx; height: 24rpx; opacity: 0.5; }
|
||
|
||
.drawer-body { display: flex; flex: 1; min-height: 0; border-top: 1rpx solid #F0F2F5; }
|
||
|
||
/* 左侧真实类目列表 */
|
||
.left-col { width: 220rpx; background: #F8F9FA; height: 100%; }
|
||
.cat-item {
|
||
padding: 28rpx 20rpx; font-size: 26rpx; color: #555; text-align: center;
|
||
border-left: 6rpx solid transparent;
|
||
}
|
||
.cat-item.on {
|
||
background: #fff; color: #F0710D; font-weight: bold; border-left-color: #F0710D;
|
||
}
|
||
.cat-name { display: block; }
|
||
|
||
/* 右侧真实服务列表 */
|
||
.right-col { flex: 1; background: #fff; height: 100%; padding: 0 24rpx; box-sizing: border-box; }
|
||
.loading-state { text-align: center; padding: 60rpx 0; }
|
||
.loading-txt { font-size: 26rpx; color: #999; }
|
||
|
||
.service-list { display: flex; flex-direction: column; }
|
||
.service-card {
|
||
padding: 26rpx 8rpx; border-bottom: 1rpx solid #F4F5F7;
|
||
display: flex; justify-content: space-between; align-items: center;
|
||
}
|
||
.sc-main { flex: 1; min-width: 0; padding-right: 20rpx; }
|
||
.sc-name { font-size: 28rpx; color: #222; font-weight: 500; display: block; line-height: 1.4; }
|
||
.sc-desc { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
|
||
.sc-price-box { display: flex; align-items: baseline; flex-shrink: 0; }
|
||
.sc-price-num { font-size: 30rpx; font-weight: bold; color: #F0710D; }
|
||
.sc-price-tag { font-size: 24rpx; color: #F0710D; font-weight: 500; }
|
||
.sc-price-unit { font-size: 22rpx; color: #888; margin-left: 2rpx; }
|
||
|
||
.empty-tip { text-align: center; padding: 80rpx 0 40rpx; }
|
||
.empty-txt { color: #999; font-size: 26rpx; }
|
||
|
||
/* 底部找不到商品引导 */
|
||
.bottom-helper {
|
||
display: flex; align-items: center; justify-content: center;
|
||
padding: 40rpx 0 60rpx; font-size: 24rpx; color: #666;
|
||
}
|
||
.bh-txt { color: #64748B; }
|
||
.bh-link { color: #F0710D; margin-left: 8rpx; font-weight: 500; }
|
||
.bh-arrow-img { width: 22rpx; height: 22rpx; margin-left: 4rpx; }
|
||
.clickable-hover:active { opacity: 0.8; }
|
||
</style>
|