feat(机柜): 添加机柜模式分类和商店列表优化

- 新增 MODE_MAP 常量定义机柜模式映射关系
- 重构商店列表为分类侧边栏+列表布局
- 添加一键全开功能
- 优化商店列表样式和响应式设计
- 修改 getShopListApi 接口参数为对象形式
- 添加 getModeListApi 接口获取模式列表
This commit is contained in:
dzq 2025-12-30 08:34:12 +08:00
parent ec44a6b0fc
commit b6f7824846
6 changed files with 355 additions and 80 deletions

View File

@ -39,6 +39,8 @@ onMounted(async () => {
let corpid = urlParams.get('corpid') || undefined;
const cid = urlParams.get('cid') || undefined;
let isAdmin = urlParams.get('isAdmin') || undefined;
//
// isAdmin = '1';
if (cid && Number(cid)) {
try {

View File

@ -99,16 +99,24 @@ export function getBalanceByQyUserid(corpid: string, userid: string) {
})
}
export function getShopListApi(corpid: string, mode?: number) {
const params: any = {
corpid
};
if (typeof mode !== 'undefined') {
params.mode = mode;
}
export interface GetShopListParams {
corpid: string;
mode?: number;
eqMode?: number;
}
export function getShopListApi(params: GetShopListParams) {
return request<ApiResponseData<ShopEntity[]>>({
url: "shop/list",
method: "get",
params
})
}
/** 获取模式列表 */
export function getModeListApi() {
return request<ApiResponseData<number[]>>({
url: "shop/mode/list",
method: "get"
})
}

View File

@ -0,0 +1,8 @@
export const MODE_MAP: Record<number, string> = {
0: '支付柜',
1: '审批柜',
2: '借还柜',
3: '会员柜',
4: '耗材柜',
5: '暂存柜',
}

View File

@ -1,20 +1,50 @@
<template>
<div v-if="showShopList" class="shop-list">
<div v-if="showShopList" class="shop-list-container van-safe-area-bottom">
<div class="shop-prompt">
<van-cell title="请选择机柜地址:" center />
</div>
<van-row :gutter="[10, 10]" class="shop-row" justify="start">
<van-col v-for="shop in shopList" :key="shop.shopId" span="12" class="shop-col">
<div class="shop-item" @click="handleShopSelect(shop.shopId)">
<van-image :src="shop.coverImg || `${publicPath}product-image.png`" class="shop-cover-img" fit="cover" />
<div class="shop-info">
<van-icon name="shop-o" size="20" class="shop-icon" />
<div class="shop-name van-ellipsis">{{ shop.shopName }}</div>
<div class="shop-content">
<!-- 左侧分类侧边栏 -->
<div class="shop-sidebar-container">
<van-sidebar v-model="activeModeIndex" class="shop-sidebar" @change="onModeChange">
<van-sidebar-item title="全部" />
<van-sidebar-item
v-for="mode in modeList"
:key="mode"
:title="MODE_MAP[mode] || `模式${mode}`"
/>
</van-sidebar>
</div>
<!-- 右侧商店列表单列 -->
<div class="shop-list-wrapper">
<div class="shop-list">
<div class="shop-item"
v-for="shop in shopList"
:key="shop.shopId"
@click="handleShopSelect(shop.shopId)">
<van-image
:src="shop.coverImg || `${publicPath}product-image.png`"
class="shop-cover-img"
fit="cover"
/>
<div class="shop-info">
<van-icon name="shop-o" size="20" class="shop-icon" />
<div class="shop-name van-ellipsis">{{ shop.shopName }}</div>
<div v-if="shop.mode !== undefined" class="shop-mode-tag">
{{ getModeLabel(shop.mode) }}
</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="shopList.length === 0" class="empty-state">
<van-empty description="暂无商店" />
</div>
</div>
</div>
</div>
</van-col>
<van-col v-if="shopList.length % 2 === 0" span="12" class="shop-col"></van-col>
</van-row>
</div>
<div v-else class="cabinet-container van-safe-area-bottom">
<div class="left-container">
@ -25,6 +55,11 @@
</div>
<div class="product-list">
<div class="product-list-header">
<van-button type="primary" icon="lock" :loading="isOpeningAll" :disabled="isOpeningAll" @click="handleOpenAllLockers">
一键全开
</van-button>
</div>
<van-cell v-for="locker in lockerList" :key="locker.lockerId" class="product-item">
<template #icon>
<div class="image-container">
@ -191,16 +226,17 @@
<script setup lang="ts">
import { throttle } from 'lodash-es';
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { getShopListApi } from '@/common/apis/shop';
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
import { getShopListApi, getModeListApi } from '@/common/apis/shop';
import { ShopEntity } from '@/common/apis/shop/type';
import { getCabinetDetailApi, openCabinet, changeGoodsCellsStock, clearGoodsCells, resetCellById } from '@/common/apis/cabinet';
import type { CabinetDetailDTO } from '@/common/apis/cabinet/type';
import { useWxStore, useWxStoreOutside } from '@/pinia/stores/wx';
import { publicPath } from "@/common/utils/path";
import { MODE_MAP } from '@/common/utils/maps/mode';
import BindGoods from './components/BindGoods.vue';
import VanPopup from 'vant/es/popup';
import { showDialog, showToast } from 'vant';
import { showConfirmDialog, showDialog, showToast } from 'vant';
const wxStore = useWxStore();
const { userid: qyUserid, name: qyName } = storeToRefs(wxStore);
@ -209,6 +245,7 @@ const activeCabinet = ref(0)
const cabinetList = ref<CabinetItem[]>([])
const lockerList = ref<LockerItem[]>([])
const openingLockerId = ref<number | null>(null)
const isOpeningAll = ref(false)
const showBindGoodsPopup = ref(false)
const currentLocker = ref<LockerItem | null>(null)
const cabinetData = ref<CabinetDetailDTO[]>([]);
@ -219,6 +256,15 @@ const shopList = ref<ShopEntity[]>([]);
const shopId = ref<number>(0);
const selectedShop = ref<ShopEntity | null>(null);
const headerHeight = ref(150);
//
const activeModeIndex = ref(0); //
const modeList = ref<number[]>([]);
const activeModeValue = computed(() => {
// 0 -> -1
// 1 -> mode...
if (activeModeIndex.value === 0) return -1;
return modeList.value[activeModeIndex.value - 1] || 0;
});
let scrollListener: any[] = [];
interface CabinetItem {
@ -302,6 +348,34 @@ function switchCellType(cellType: number | undefined) {
}
}
// mode
const loadModeList = async () => {
try {
const res = await getModeListApi();
if (res?.code === 0 && res?.data?.length > 0) {
modeList.value = res.data;
} else {
modeList.value = [];
}
} catch (error) {
console.error('获取模式列表失败:', error);
modeList.value = [];
}
};
// mode
const getModeLabel = (mode: number | undefined): string => {
if (mode === undefined) return '';
return MODE_MAP[mode] || '';
};
//
const onModeChange = (index: number) => {
// activeModeIndex v-model
//
getShopList();
};
const showBindGoods = (locker: LockerItem) => {
currentLocker.value = locker;
showBindGoodsPopup.value = true;
@ -315,7 +389,6 @@ const handleBindSuccess = () => {
const handleOpenLocker = async (locker: LockerItem) => {
openingLockerId.value = locker.lockerId
try {
//
await openCabinet(cabinetList.value[activeCabinet.value].cabinetId, locker.lockerNumber, {
cellId: locker.lockerId,
userid: qyUserid.value,
@ -331,8 +404,49 @@ const handleOpenLocker = async (locker: LockerItem) => {
}
}
const handleOpenAllLockers = async () => {
if (isOpeningAll.value || lockerList.value.length === 0) return;
await showConfirmDialog({
title: '确认开启',
message: `确定要开启所有 ${lockerList.value.length} 个格口吗?`
});
isOpeningAll.value = true;
try {
for (let i = 0; i < lockerList.value.length; i++) {
const locker = lockerList.value[i];
openingLockerId.value = locker.lockerId;
try {
await openCabinet(cabinetList.value[activeCabinet.value].cabinetId, locker.lockerNumber, {
cellId: locker.lockerId,
userid: qyUserid.value,
isInternal: 2,
name: qyName.value,
mobile: '',
operationType: 2
});
} catch (error) {
console.error(`打开格口 ${locker.cellNo} 失败:`, error);
}
if (i < lockerList.value.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
} catch (error) {
console.error('一键全开失败:', error);
} finally {
openingLockerId.value = null;
isOpeningAll.value = false;
}
}
//
const init = async () => {
await loadModeList();
if (showShopList.value) {
await getShopList();
} else if (shopId.value) {
@ -343,12 +457,17 @@ const init = async () => {
//
const getShopList = async () => {
try {
const res = await getShopListApi(wxStore.corpid, -1);
// 使mode-1""
const modeParam = activeModeValue.value === -1 ? -1 : activeModeValue.value;
const res = await getShopListApi({ corpid: wxStore.corpid, eqMode: modeParam });
if (res?.code === 0 && res?.data?.length > 0) {
shopList.value = res.data;
} else {
shopList.value = [];
}
} catch (error) {
console.error('获取商店列表失败:', error);
shopList.value = [];
}
};
@ -367,6 +486,8 @@ const handleShopSelect = (selectedShopId: number) => {
const handleBackToShopList = () => {
showShopList.value = true;
shopId.value = 0;
// ""
activeModeIndex.value = 0;
};
//
@ -452,60 +573,15 @@ onBeforeUnmount(() => {
scale: calc(1 + (150 - var(--header-height)) / 150);
}
.shop-list {
.shop-prompt {
margin: 8px;
height: 44px !important;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
}
.shop-row {
margin: 8px 0;
padding: 0 8px;
}
.shop-col {
margin-bottom: 8px;
}
.shop-item {
height: auto !important;
padding: 0;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
.shop-cover-img {
width: 100%;
height: 80px;
object-fit: cover;
}
.shop-info {
display: flex;
align-items: center;
justify-content: start;
padding: 12px;
}
.shop-name {
white-space: normal;
word-break: break-word;
font-size: 14px;
color: #333;
font-weight: 500;
margin: 2px 0 4px 4px;
display: flex;
align-items: center;
justify-content: center;
}
}
/* 商店列表提示 */
.shop-prompt {
margin: 8px;
height: 44px !important;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s;
}
.showShopListBtn {
@ -611,6 +687,12 @@ onBeforeUnmount(() => {
background: #ffffff;
}
.product-list-header {
padding: 8px 0;
display: flex;
justify-content: center;
}
.product-item {
margin-bottom: 10px;
padding: min(2.667vw, 20px) 0;
@ -690,4 +772,179 @@ onBeforeUnmount(() => {
color: #999;
border-color: #999;
}
/* 商店列表容器 */
.shop-list-container {
display: flex;
flex-direction: column;
height: calc(100vh - var(--van-tabbar-height));
}
/* 主要内容区域 */
.shop-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* 左侧分类侧边栏 */
.shop-sidebar-container {
width: 90px;
background: #f7f8fa;
border-right: 1px solid #ebedf0;
overflow-y: auto;
.shop-sidebar {
width: 100%;
:deep(.van-sidebar-item) {
padding: 16px 8px;
font-size: 14px;
line-height: 1.4;
word-break: break-all;
&.van-sidebar-item--select {
background-color: #ffffff;
color: var(--van-primary-color);
font-weight: 500;
&::before {
content: '';
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 3px;
height: 16px;
background-color: var(--van-primary-color);
border-radius: 0 2px 2px 0;
}
}
}
}
}
/* 右侧商店列表区域 */
.shop-list-wrapper {
flex: 1;
overflow-y: auto;
padding: 8px;
background: #ffffff;
}
.shop-list {
.shop-item {
margin-bottom: 12px;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12);
}
.shop-cover-img {
width: 100%;
height: 120px;
object-fit: cover;
}
.shop-info {
display: flex;
align-items: center;
padding: 16px;
.shop-icon {
color: var(--van-primary-color);
margin-right: 8px;
flex-shrink: 0;
}
.shop-name {
flex: 1;
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.shop-mode-tag {
margin-left: 8px;
padding: 2px 8px;
background: var(--van-primary-color);
color: white;
border-radius: 12px;
font-size: 12px;
flex-shrink: 0;
}
}
}
}
/* 空状态 */
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
}
/* 移动端适配 - 针对小屏幕优化 */
@media (max-width: 375px) {
.shop-sidebar-container {
width: 80px;
.shop-sidebar {
:deep(.van-sidebar-item) {
padding: 12px 6px;
font-size: 13px;
}
}
}
.shop-item {
.shop-cover-img {
height: 100px !important;
}
.shop-info {
padding: 12px !important;
.shop-name {
font-size: 15px !important;
}
.shop-mode-tag {
font-size: 11px !important;
padding: 2px 6px !important;
}
}
}
}
/* 横屏适配 */
@media (orientation: landscape) and (max-height: 500px) {
.shop-list-container {
height: calc(100vh - var(--van-nav-bar-height));
}
.shop-sidebar-container {
width: 100px;
.shop-sidebar {
:deep(.van-sidebar-item) {
padding: 10px 6px;
font-size: 12px;
}
}
}
}
</style>

View File

@ -69,7 +69,7 @@ const init = async () => {
const loadShopList = async () => {
loading.value = true
try {
const res = await getShopListApi(wxStore.corpid || 'wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw', -1)
const res = await getShopListApi({ corpid: wxStore.corpid || 'wpZ1ZrEgAA2QTxIRcB4cMtY7hQbTcPAw', mode: -1 })
if (res?.code === 0 && res?.data?.length > 0) {
shopList.value = res.data.filter(shop => shop.mode === 5)
}

View File

@ -68,7 +68,7 @@ onMounted(async () => {
if (showShopList.value) {
// handleWxCallback
await wxStore.waitForHandleWxCallbackComplete();
getShopListApi(wxStore.corpid).then((res) => {
getShopListApi({ corpid: wxStore.corpid }).then((res) => {
if (res?.code === 0 && res?.data?.length > 0) {
shopList.value = res.data;
if (shopIdParam && shopList.value.some(shop => shop.shopId.toString() == shopIdParam)) {
@ -102,7 +102,7 @@ watch(() => route.path, async (newPath) => {
if (showShopList.value) {
// handleWxCallback
await wxStore.waitForHandleWxCallbackComplete();
getShopListApi(wxStore.corpid).then((res) => {
getShopListApi({ corpid: wxStore.corpid }).then((res) => {
if (res?.code === 0 && res?.data?.length > 0) {
shopList.value = res.data;
}