feat(cabinet): 添加格口备注功能

- 在类型定义中为 CabinetCellEntity、CellInfoDTO 和 AvailableStorageCellDTO 添加 remark 字段
- 新增 UpdateCellRemarkDTO 类型用于更新格口备注
- 添加 updateCellRemark API 方法
- 在格口列表页添加备注显示
- 实现格口点击弹窗功能,包含备注编辑和保存
- 添加相关样式和状态管理
This commit is contained in:
dzq 2025-12-31 17:47:53 +08:00
parent 7eb6521dfd
commit 3d99b20754
3 changed files with 198 additions and 23 deletions

View File

@ -6,7 +6,8 @@ import type {
StoreItemToCellCommand, StoreItemToCellCommand,
OpenCellByPasswordCommand, OpenCellByPasswordCommand,
ResetCellByPasswordCommand, ResetCellByPasswordCommand,
CabinetCellEntity CabinetCellEntity,
UpdateCellRemarkDTO
} from './type' } from './type'
import { OpenCabinetApiData } from '../shop/type' import { OpenCabinetApiData } from '../shop/type'
@ -54,32 +55,41 @@ export function openCabinet(cabinetId: number, pinNo: number, data: OpenCabinetA
export const configureGoodsCellsStock = (cellId: number, goodsId: number, stock: number) => { export const configureGoodsCellsStock = (cellId: number, goodsId: number, stock: number) => {
return request<ApiResponseData<void>>({ return request<ApiResponseData<void>>({
url: `/cabinet/configureGoodsCellsStock/${cellId}/${goodsId}/${stock}`, url: `cabinet/configureGoodsCellsStock/${cellId}/${goodsId}/${stock}`,
method: 'put' method: 'put'
}); });
}; };
export const changeGoodsCellsStock = (cellId: number, stock: number) => { export const changeGoodsCellsStock = (cellId: number, stock: number) => {
return request<ApiResponseData<void>>({ return request<ApiResponseData<void>>({
url: `/cabinet/changeGoodsCellsStock/${cellId}/${stock}`, url: `cabinet/changeGoodsCellsStock/${cellId}/${stock}`,
method: 'put' method: 'put'
}); });
}; };
export const clearGoodsCells = (cellId: number) => { export const clearGoodsCells = (cellId: number) => {
return request<ApiResponseData<void>>({ return request<ApiResponseData<void>>({
url: `/cabinet/clearGoodsCells/${cellId}`, url: `cabinet/clearGoodsCells/${cellId}`,
method: 'put' method: 'put'
}); });
}; };
export const resetCellById = (cellId: number) => { export const resetCellById = (cellId: number) => {
return request<ApiResponseData<void>>({ return request<ApiResponseData<void>>({
url: `/cabinet/reset/${cellId}`, url: `cabinet/reset/${cellId}`,
method: 'put' method: 'put'
}); });
}; };
/** 临时:更新格口备注 */
export const updateCellRemark = (data: UpdateCellRemarkDTO) => {
return request<ApiResponseData<void>>({
url: `cabinet/cell`,
method: 'put',
data
});
};
/** 获取可用暂存柜格口列表 */ /** 获取可用暂存柜格口列表 */
export function availableStorageCells(shopId: number) { export function availableStorageCells(shopId: number) {
return request<ApiResponseData<AvailableStorageCellDTO[]>>({ return request<ApiResponseData<AvailableStorageCellDTO[]>>({

View File

@ -56,6 +56,8 @@ export interface CabinetCellEntity {
goodsId?: number goodsId?: number
/** 密码 */ /** 密码 */
password?: string password?: string
/** 备注 */
remark?: string
} }
export interface CellInfoDTO { export interface CellInfoDTO {
@ -75,6 +77,8 @@ export interface CellInfoDTO {
usageStatus: number usageStatus: number
/** 格口类型1小格 2中格 3大格 4超大格 */ /** 格口类型1小格 2中格 3大格 4超大格 */
cellType: number cellType: number
/** 备注 */
remark?: string
} }
export interface ProductInfoDTO { export interface ProductInfoDTO {
@ -122,6 +126,8 @@ export interface AvailableStorageCellDTO {
price: number price: number
/** 封面图URL */ /** 封面图URL */
coverImg: string coverImg: string
/** 备注 */
remark?: string
} }
/** 存入物品分配格口请求参数 */ /** 存入物品分配格口请求参数 */
@ -147,3 +153,35 @@ export interface ResetCellByPasswordCommand {
/** 格口密码 */ /** 格口密码 */
password: string password: string
} }
/** 更新格口备注请求参数cellId 必需,其他字段可选) */
export interface UpdateCellRemarkDTO {
/** 格口唯一ID */
cellId: number
/** 关联柜机ID */
cabinetId?: number
/** 主板ID */
mainboardId?: number
/** 格口号 */
cellNo?: number
/** 针脚序号 */
pinNo?: number
/** 库存数量 */
stock?: number
/** 格口价格 */
cellPrice?: number
/** 是否已租用0-未租用1-已租用 */
isRented?: number
/** 格口类型1小格 2中格 3大格 4超大格 */
cellType?: number
/** 使用状态1空闲 2已占用 */
usageStatus?: number
/** 可用状态1正常 2故障 */
availableStatus?: number
/** 商品ID */
goodsId?: number
/** 密码 */
password?: string
/** 备注 */
remark?: string
}

View File

@ -54,7 +54,7 @@
一键全开 一键全开
</van-button> </van-button>
</div> </div>
<van-cell v-for="locker in lockerList" :key="locker.lockerId" class="product-item"> <van-cell v-for="locker in lockerList" :key="locker.lockerId" class="product-item" @click="handleLockerClick(locker)">
<template #icon> <template #icon>
<div class="image-container"> <div class="image-container">
<van-image width="80" height="80" v-if="locker.coverImg" <van-image width="80" height="80" v-if="locker.coverImg"
@ -183,22 +183,8 @@
<!-- <div class="goods-name">{{ locker.usageStatus === 1 ? '空闲' : '占用' }}</div> --> <!-- <div class="goods-name">{{ locker.usageStatus === 1 ? '空闲' : '占用' }}</div> -->
</div> </div>
</div> </div>
<div class="button-group"> <div class="remark-info">
<van-button v-if="selectedShop?.mode !== 5" size="small" type="primary" class="detail-btn" 备注: {{ locker.remark || '-' }}
@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)" style="margin-left: auto;">
立即开启
</van-button>
</div> </div>
</div> </div>
</van-cell> </van-cell>
@ -211,6 +197,48 @@
@cancel="showBindGoodsPopup = false" @success="handleBindSuccess" @unbind="handleBindSuccess" @cancel="showBindGoodsPopup = false" @success="handleBindSuccess" @unbind="handleBindSuccess"
@update:currentStock="handleStockUpdate" /> @update:currentStock="handleStockUpdate" />
</VanPopup> </VanPopup>
<!-- 格口控制弹窗 -->
<VanPopup v-model:show="showLockerPopup" position="bottom" :style="{ height: '45%' }" round>
<div class="locker-popup-content" v-if="selectedLocker">
<div class="popup-header">
<span class="popup-title">格口 {{ selectedLocker.cellNo }}</span>
<van-icon name="cross" @click="showLockerPopup = false" />
</div>
<div class="locker-info">
<div class="info-row">
<span>状态: {{ selectedLocker.usageStatus === 1 ? '空闲' : '占用' }}</span>
<span v-if="selectedLocker.goodsName">商品: {{ selectedLocker.goodsName }}</span>
</div>
<div class="info-row" v-if="selectedShop?.mode !== 5">
<span>价格: ¥{{ (selectedLocker.price || 0).toFixed(2) }}</span>
</div>
</div>
<van-field v-model="editingRemark" label="备注" placeholder="请输入备注" rows="2" type="textarea"
class="remark-field" />
<div class="popup-actions">
<van-button type="primary" :loading="openingLockerId === selectedLocker.lockerId"
@click="handleOpenLocker(selectedLocker)" style="flex: 1;">
立即开启
</van-button>
<van-button v-if="selectedShop?.mode === 5 && selectedLocker.password" plain
@click="handleClearPassword(selectedLocker)" style="flex: 1;">
清除密码
</van-button>
<van-button v-else-if="selectedShop?.mode !== 5" plain
@click="handleBindFromLocker(selectedLocker)" style="flex: 1;">
绑定商品
</van-button>
</div>
<van-button @click="handleSaveRemark" :loading="isSavingRemark" style="margin-top: 8px;">
保存备注
</van-button>
</div>
</VanPopup>
</div> </div>
</template> </template>
@ -219,7 +247,7 @@ import { throttle } from 'lodash-es';
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'; import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
import { getShopListApi, getModeListApi } from '@/common/apis/shop'; import { getShopListApi, getModeListApi } from '@/common/apis/shop';
import { ShopEntity } from '@/common/apis/shop/type'; import { ShopEntity } from '@/common/apis/shop/type';
import { getCabinetDetailApi, openCabinet, changeGoodsCellsStock, clearGoodsCells, resetCellById } from '@/common/apis/cabinet'; import { getCabinetDetailApi, openCabinet, changeGoodsCellsStock, clearGoodsCells, resetCellById, updateCellRemark } from '@/common/apis/cabinet';
import type { CabinetDetailDTO } from '@/common/apis/cabinet/type'; import type { CabinetDetailDTO } from '@/common/apis/cabinet/type';
import { useWxStore, useWxStoreOutside } from '@/pinia/stores/wx'; import { useWxStore, useWxStoreOutside } from '@/pinia/stores/wx';
import { useRouterStore } from '@/pinia/stores/router'; import { useRouterStore } from '@/pinia/stores/router';
@ -249,6 +277,11 @@ const shopList = ref<ShopEntity[]>([]);
const shopId = ref<number>(0); const shopId = ref<number>(0);
const selectedShop = ref<ShopEntity | null>(null); const selectedShop = ref<ShopEntity | null>(null);
const headerHeight = ref(150); const headerHeight = ref(150);
//
const showLockerPopup = ref(false)
const selectedLocker = ref<LockerItem | null>(null)
const editingRemark = ref('')
const isSavingRemark = ref(false)
// //
const activeModeIndex = ref(0); // const activeModeIndex = ref(0); //
const modeList = ref<number[]>([]); const modeList = ref<number[]>([]);
@ -279,6 +312,7 @@ interface LockerItem {
password?: string password?: string
usageStatus?: number usageStatus?: number
cellType?: number cellType?: number
remark?: string
} }
// //
@ -320,6 +354,7 @@ const updateLockerList = (cabinet: CabinetDetailDTO) => {
password: cell.password, password: cell.password,
usageStatus: cell.usageStatus, usageStatus: cell.usageStatus,
cellType: cell.cellType, cellType: cell.cellType,
remark: cell.remark,
})) }))
} }
@ -509,6 +544,42 @@ const handleClearPassword = async (locker: LockerItem) => {
} }
}; };
const handleBindFromLocker = (locker: LockerItem) => {
currentLocker.value = locker;
showLockerPopup.value = false;
showBindGoodsPopup.value = true;
};
//
const handleLockerClick = (locker: LockerItem) => {
selectedLocker.value = locker
editingRemark.value = locker.remark || ''
showLockerPopup.value = true
}
//
const handleSaveRemark = async () => {
if (!selectedLocker.value) return
isSavingRemark.value = true
try {
await updateCellRemark({
cellId: selectedLocker.value.lockerId,
remark: editingRemark.value
})
showToast('保存成功')
//
if (selectedLocker.value) {
selectedLocker.value.remark = editingRemark.value
}
loadCabinetDetail()
showLockerPopup.value = false
} catch (error) {
console.error('保存备注失败:', error)
} finally {
isSavingRemark.value = false
}
}
onMounted(() => { onMounted(() => {
init(); init();
scrollListener.push(window.addEventListener('scroll', throttledScroll)); scrollListener.push(window.addEventListener('scroll', throttledScroll));
@ -656,6 +727,13 @@ onBeforeUnmount(() => {
} }
} }
.remark-info {
text-align: left;
font-size: 12px;
color: #666;
margin-top: 8px;
}
.button-group { .button-group {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -944,4 +1022,53 @@ onBeforeUnmount(() => {
} }
} }
} }
/* 格口控制弹窗样式 */
.locker-popup-content {
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
height: 100%;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12px;
border-bottom: 1px solid #ebedf0;
.popup-title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.van-icon {
font-size: 20px;
color: #999;
padding: 4px;
}
}
.locker-info {
.info-row {
display: flex;
gap: 16px;
margin-bottom: 8px;
font-size: 14px;
color: #666;
}
}
.remark-field {
margin-top: 8px;
}
.popup-actions {
display: flex;
gap: 12px;
margin-top: 4px;
}
</style> </style>