feat(柜机管理): 新增格口状态展示及密码清除功能
- 在柜机管理页面添加格口状态可视化展示,包括空闲和占用状态的SVG图标 - 新增resetCellById API用于清除格口密码 - 优化商品列表加载逻辑,避免重复数据 - 完善类型定义,添加格口状态和类型相关字段 - 根据店铺模式调整UI显示,隐藏部分非必要元素 - 添加清除密码按钮及相关处理逻辑
This commit is contained in:
parent
cb20e28890
commit
8866478706
|
|
@ -65,3 +65,10 @@ export const clearGoodsCells = (cellId: number) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const resetCellById = (cellId: number) => {
|
||||
return request<ApiResponseData<void>>({
|
||||
url: `/cabinet/reset/${cellId}`,
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
export interface CabinetDetailDTO {
|
||||
/** 柜机ID */
|
||||
cabinetId: number
|
||||
/** 柜机名称 */
|
||||
cabinetName: string
|
||||
/** 锁控编号 */
|
||||
lockControlNo: number
|
||||
/** 格口列表 */
|
||||
cells: CellInfoDTO[]
|
||||
}
|
||||
|
||||
|
|
@ -14,11 +18,13 @@ export interface RentingCabinetDetailDTO {
|
|||
/** 锁控编号 */
|
||||
lockControlNo: number
|
||||
/** 柜格列表 */
|
||||
cells: RetingCellEntity[]
|
||||
cells: RentingCellEntity[]
|
||||
}
|
||||
|
||||
export interface RetingCellEntity extends CabinetCellEntity {
|
||||
export interface RentingCellEntity extends CabinetCellEntity {
|
||||
/** 订单ID */
|
||||
orderId: number;
|
||||
/** 订单商品ID */
|
||||
orderGoodsId: number;
|
||||
}
|
||||
|
||||
|
|
@ -48,14 +54,27 @@ export interface CabinetCellEntity {
|
|||
availableStatus: number
|
||||
/** 商品ID */
|
||||
goodsId?: number
|
||||
/** 密码 */
|
||||
password?: string
|
||||
}
|
||||
|
||||
export interface CellInfoDTO {
|
||||
/** 格口唯一ID */
|
||||
cellId: number
|
||||
/** 格口号 */
|
||||
cellNo: number
|
||||
/** 针脚序号 */
|
||||
pinNo: number
|
||||
/** 库存数量 */
|
||||
stock: number
|
||||
/** 密码 */
|
||||
password?: string
|
||||
/** 商品信息 */
|
||||
product?: ProductInfoDTO
|
||||
/** 使用状态:1空闲 2已占用 */
|
||||
usageStatus: number
|
||||
/** 格口类型(1小格 2中格 3大格 4超大格) */
|
||||
cellType: number
|
||||
}
|
||||
|
||||
export interface ProductInfoDTO {
|
||||
|
|
|
|||
|
|
@ -29,11 +29,108 @@
|
|||
<template #icon>
|
||||
<div class="image-container">
|
||||
<van-image width="80" height="80"
|
||||
v-if="locker.coverImg"
|
||||
:src="locker.coverImg ? locker.coverImg : `${publicPath}` + 'img/product-image.svg'"
|
||||
fit="cover" radius="4" class="product-image"
|
||||
:style="{ filter: locker.stock === 0 ? 'grayscale(100%)' : 'none' }">
|
||||
:style="{ filter: locker.stock === 0 && locker.usageStatus === 1 ? 'grayscale(100%)' : 'none' }">
|
||||
</van-image>
|
||||
<div v-if="locker.stock >= 0" class="stock-overlay">
|
||||
<svg v-if="!locker.coverImg && locker.usageStatus === 2" width="80" height="80"
|
||||
viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
|
||||
:class="['cell-image', selectedShop?.mode === 5 ? 'cell-image-full' : '']">
|
||||
<defs>
|
||||
<linearGradient id="occupiedGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#FFF3E0;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#FFE0B2;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="1" dy="1" stdDeviation="1.5" flood-color="#000000" flood-opacity="0.1" />
|
||||
</filter>
|
||||
<filter id="innerShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feOffset dx="0" dy="0.5" />
|
||||
<feGaussianBlur stdDeviation="0.8" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="arithmetic" k2="-1" k3="1" />
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- 主卡片背景 -->
|
||||
<rect x="5" y="5" width="90" height="90" rx="12" fill="url(#occupiedGradient)" stroke="#FF9800"
|
||||
stroke-width="2.5" filter="url(#shadow)" />
|
||||
<!-- 内阴影效果 -->
|
||||
<rect x="5" y="5" width="90" height="90" rx="12" fill="none" stroke="rgba(255,152,0,0.3)"
|
||||
stroke-width="1" filter="url(#innerShadow)" />
|
||||
|
||||
<!-- 锁定图标 -->
|
||||
<g transform="translate(35, 12)">
|
||||
<rect x="8" y="12" width="16" height="12" rx="2" fill="#FF6F00" stroke="#E65100"
|
||||
stroke-width="1" />
|
||||
<path d="M 12 12 L 12 8 C 12 5.79 13.79 4 16 4 L 16 4 C 18.21 4 20 5.79 20 8 L 20 12" fill="none"
|
||||
stroke="#FF6F00" stroke-width="2" stroke-linecap="round" />
|
||||
</g>
|
||||
|
||||
<!-- 状态文字 -->
|
||||
<text x="50" y="54" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#E65100"
|
||||
text-anchor="middle">占用</text>
|
||||
|
||||
<!-- 装饰性分割线 -->
|
||||
<line x1="20" y1="62" x2="80" y2="62" stroke="#FF9800" stroke-width="1.5" stroke-dasharray="3,2"
|
||||
opacity="0.7" />
|
||||
|
||||
<!-- 格口类型标签 -->
|
||||
<g transform="translate(50, 78)">
|
||||
<rect x="-18" y="-8" width="36" height="16" rx="8" fill="#FF9800" opacity="0.9" />
|
||||
<text x="0" y="3" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="white"
|
||||
text-anchor="middle">{{ switchCellType(locker.cellType) }}</text>
|
||||
</g>
|
||||
</svg>
|
||||
<svg v-if="!locker.coverImg && locker.usageStatus === 1" width="80" height="80"
|
||||
viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"
|
||||
:class="['cell-image', selectedShop?.mode === 5 ? 'cell-image-full' : '']">
|
||||
<defs>
|
||||
<linearGradient id="freeGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E8F5E9;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#C8E6C9;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow2" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="1" dy="1" stdDeviation="1.5" flood-color="#000000" flood-opacity="0.1" />
|
||||
</filter>
|
||||
<filter id="innerShadow2" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feOffset dx="0" dy="0.5" />
|
||||
<feGaussianBlur stdDeviation="0.8" result="blur" />
|
||||
<feComposite in="SourceGraphic" in2="blur" operator="arithmetic" k2="-1" k3="1" />
|
||||
</filter>
|
||||
</defs>
|
||||
<!-- 主卡片背景 -->
|
||||
<rect x="5" y="5" width="90" height="90" rx="12" fill="url(#freeGradient)" stroke="#4CAF50"
|
||||
stroke-width="2.5" filter="url(#shadow2)" />
|
||||
<!-- 内阴影效果 -->
|
||||
<rect x="5" y="5" width="90" height="90" rx="12" fill="none" stroke="rgba(76,175,80,0.3)"
|
||||
stroke-width="1" filter="url(#innerShadow2)" />
|
||||
|
||||
<!-- 开锁图标 -->
|
||||
<g transform="translate(35, 12)">
|
||||
<rect x="8" y="12" width="16" height="12" rx="2" fill="#4CAF50" stroke="#388E3C"
|
||||
stroke-width="1" />
|
||||
<path d="M 12 12 L 12 8 C 12 5.79 13.79 4 16 4 L 16 4 C 18.21 4 20 5.79 20 8 L 20 12" fill="none"
|
||||
stroke="#4CAF50" stroke-width="2" stroke-linecap="round" />
|
||||
<!-- 打开的锁舌 -->
|
||||
<circle cx="16" cy="18" r="2" fill="#4CAF50" stroke="#388E3C" stroke-width="1" />
|
||||
</g>
|
||||
|
||||
<!-- 状态文字 -->
|
||||
<text x="50" y="54" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#2E7D32"
|
||||
text-anchor="middle">空闲</text>
|
||||
|
||||
<!-- 装饰性分割线 -->
|
||||
<line x1="20" y1="62" x2="80" y2="62" stroke="#4CAF50" stroke-width="1.5" stroke-dasharray="3,2"
|
||||
opacity="0.7" />
|
||||
|
||||
<!-- 格口类型标签 -->
|
||||
<g transform="translate(50, 78)">
|
||||
<rect x="-18" y="-8" width="36" height="16" rx="8" fill="#4CAF50" opacity="0.9" />
|
||||
<text x="0" y="3" font-family="Arial, sans-serif" font-size="10" font-weight="bold" fill="white"
|
||||
text-anchor="middle">{{ switchCellType(locker.cellType) }}</text>
|
||||
</g>
|
||||
</svg>
|
||||
<div v-if="locker.stock >= 0 && selectedShop?.mode !==5" class="stock-overlay">
|
||||
库存: {{ locker.stock }}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -43,25 +140,30 @@
|
|||
<div v-if="locker.goodsName">
|
||||
<div class="info-row">
|
||||
<div class="locker-number">格口 {{ locker.cellNo }}</div>
|
||||
<div class="goods-price">¥{{ (locker.price || 0).toFixed(2) }}</div>
|
||||
<div v-if="selectedShop?.mode !==5" class="goods-price">¥{{ (locker.price || 0).toFixed(2) }}</div>
|
||||
</div>
|
||||
<div class="goods-name">{{ locker.goodsName }}</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="info-row">
|
||||
<div class="locker-number">格口 {{ locker.cellNo }}</div>
|
||||
<div class="goods-price">¥0.00</div>
|
||||
<div v-if="selectedShop?.mode !==5" class="goods-price">¥0.00</div>
|
||||
</div>
|
||||
<div class="goods-name">空闲</div>
|
||||
<!-- <div class="goods-name">{{ locker.usageStatus === 1 ? '空闲' : '占用' }}</div> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<van-button size="small" type="primary" class="detail-btn" @click="showBindGoods(locker)">
|
||||
<van-button v-if="selectedShop?.mode !== 5" size="small" type="primary" class="detail-btn" @click="showBindGoods(locker)">
|
||||
绑定商品
|
||||
</van-button>
|
||||
<van-button v-if="selectedShop?.mode === 5 && locker.password" size="small" type="primary" class="detail-btn" @click="handleClearPassword(locker)">
|
||||
清除密码
|
||||
</van-button>
|
||||
<div v-else-if="selectedShop?.mode === 5 && !locker.password" class="detail-btn-placeholder"></div>
|
||||
|
||||
<van-button size="small" plain hairline :loading="openingLockerId === locker.lockerId"
|
||||
@click="handleOpenLocker(locker)">
|
||||
@click="handleOpenLocker(locker)"
|
||||
style="margin-left: auto;">
|
||||
立即开启
|
||||
</van-button>
|
||||
</div>
|
||||
|
|
@ -71,18 +173,18 @@
|
|||
|
||||
<VanPopup v-model:show="showBindGoodsPopup" position="bottom" :style="{ height: '95%' }" round>
|
||||
<BindGoods
|
||||
v-if="currentLocker"
|
||||
:shop-id="shopId"
|
||||
:locker-id="currentLocker.lockerId"
|
||||
:visible="showBindGoodsPopup"
|
||||
:current-stock="currentLocker.stock"
|
||||
:current-goods-name="currentLocker.goodsName"
|
||||
:mode="selectedShop?.mode"
|
||||
@cancel="showBindGoodsPopup = false"
|
||||
@success="handleBindSuccess"
|
||||
@unbind="handleBindSuccess"
|
||||
@update:currentStock="handleStockUpdate"
|
||||
/>
|
||||
v-if="currentLocker"
|
||||
:shop-id="shopId"
|
||||
:locker-id="currentLocker.lockerId"
|
||||
:visible="showBindGoodsPopup"
|
||||
:current-stock="currentLocker.stock"
|
||||
:current-goods-name="currentLocker.goodsName"
|
||||
:mode="selectedShop?.mode"
|
||||
@cancel="showBindGoodsPopup = false"
|
||||
@success="handleBindSuccess"
|
||||
@unbind="handleBindSuccess"
|
||||
@update:currentStock="handleStockUpdate"
|
||||
/>
|
||||
</VanPopup>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -92,7 +194,7 @@ import { throttle } from 'lodash-es';
|
|||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||
import { getShopListApi } from '@/common/apis/shop';
|
||||
import { ShopEntity } from '@/common/apis/shop/type';
|
||||
import { getCabinetDetailApi, openCabinet, changeGoodsCellsStock, clearGoodsCells } from '@/common/apis/cabinet';
|
||||
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";
|
||||
|
|
@ -135,6 +237,9 @@ interface LockerItem {
|
|||
goodsName?: string
|
||||
price?: number
|
||||
coverImg?: string
|
||||
password?: string
|
||||
usageStatus?: number
|
||||
cellType?: number
|
||||
}
|
||||
|
||||
// 获取机柜列表
|
||||
|
|
@ -151,8 +256,10 @@ const loadCabinetDetail = async (selectedShopId?: number) => {
|
|||
}))
|
||||
|
||||
// 根据当前选中柜机加载格口数据
|
||||
if (data.length > 0) {
|
||||
if (data.length > 0 && data[activeCabinet.value]) {
|
||||
updateLockerList(data[activeCabinet.value])
|
||||
} else {
|
||||
lockerList.value = [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取柜机详情失败:', error)
|
||||
|
|
@ -170,7 +277,10 @@ const updateLockerList = (cabinet: CabinetDetailDTO) => {
|
|||
statusClass: cell.product ? 'occupied' : 'available',
|
||||
goodsName: cell.product?.goodsName,
|
||||
price: cell.product?.price,
|
||||
coverImg: cell.product?.coverImg
|
||||
coverImg: cell.product?.coverImg,
|
||||
password: cell.password,
|
||||
usageStatus: cell.usageStatus,
|
||||
cellType: cell.cellType,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +292,16 @@ const onCabinetChange = (index: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
function switchCellType(cellType: number | undefined) {
|
||||
switch (cellType) {
|
||||
case 1: return '小格';
|
||||
case 2: return '中格';
|
||||
case 3: return '大格';
|
||||
case 4: return '超大格';
|
||||
default: return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
const showBindGoods = (locker: LockerItem) => {
|
||||
currentLocker.value = locker;
|
||||
showBindGoodsPopup.value = true;
|
||||
|
|
@ -264,6 +384,17 @@ const handleStockUpdate = (newStock: number) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClearPassword = async (locker: LockerItem) => {
|
||||
try {
|
||||
await resetCellById(locker.lockerId);
|
||||
showToast('清除密码成功');
|
||||
loadCabinetDetail();
|
||||
} catch (error) {
|
||||
console.error('清除密码失败:', error);
|
||||
showToast('清除密码失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
scrollListener.push(window.addEventListener('scroll', throttledScroll));
|
||||
|
|
@ -463,6 +594,10 @@ onBeforeUnmount(() => {
|
|||
border: none;
|
||||
}
|
||||
|
||||
.detail-btn-placeholder {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.van-button--default) {
|
||||
color: #e95d5d;
|
||||
border-color: #e95d5d;
|
||||
|
|
|
|||
|
|
@ -48,17 +48,12 @@ onUnmounted(() => {
|
|||
debouncedFetchGoodsList.cancel();
|
||||
});
|
||||
|
||||
// 搜索按钮触发
|
||||
const handleSearch = () => {
|
||||
debouncedFetchGoodsList.cancel();
|
||||
fetchGoodsList();
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const onLoad = async () => {
|
||||
try {
|
||||
const res = await getGoodsList(searchParams);
|
||||
goodsList.value.push(...res.data.rows);
|
||||
const rows = res.data.rows.filter(row => goodsList.value.findIndex(g => g.goodsId === row.goodsId) === -1);
|
||||
goodsList.value.push(...rows);
|
||||
loading.value = false;
|
||||
|
||||
if (goodsList.value.length >= res.data.total) {
|
||||
|
|
@ -108,7 +103,7 @@ onMounted(fetchGoodsList);
|
|||
<div class="goods-manage">
|
||||
<!-- 搜索栏和操作按钮 -->
|
||||
<div class="search-action-bar">
|
||||
<van-search v-model="searchParams.goodsName" placeholder="输入商品名称搜索" @search="handleSearch" />
|
||||
<van-search v-model="searchParams.goodsName" placeholder="输入商品名称搜索" />
|
||||
<van-button type="primary" @click="navigateToEdit">添加商品</van-button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue