文件
yjyf/yjyf-app/components/GoodsPickerDrawer.vue
YuanWen 9ae2b976a8 feat: 重构首页与全局UI,新增服务选择抽屉与统一导航组件
新增统一导航栏组件YjyfNavBar,替换各页面原生导航
新增服务选择抽屉组件GoodsPickerDrawer,实现分类选品流程
新增tabbar图标矢量资源,优化底部导航样式
新增后端运力统计接口与多商品下单逻辑
修复页面跳转逻辑,统一使用switchTab/redirectTo
优化订单创建与评价页的UI组件化,提升一致性
2026-11-02 18:27:37 +08:00

304 行
9.5 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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>