feat(智能柜管理): 重构单元格商品管理界面并新增商品配置模态框

- 将单元格列表从表格布局改为卡片式布局,提升可视化效果
- 新增商品配置模态框用于管理已配置商品的库存和下架操作
- 优化单元格编辑模态框,增加商品信息展示和库存修改功能
- 添加默认商品图片占位符
- 调整分页参数和样式,优化移动端显示效果
This commit is contained in:
dzq 2025-06-04 11:50:25 +08:00
parent 8c8f65be7f
commit 9d5f885766
4 changed files with 288 additions and 95 deletions

View File

@ -0,0 +1,6 @@
<svg width="80" height="80" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect x="5" y="5" width="90" height="90" rx="10" fill="#E8F5E9" stroke="#81C784"
stroke-width="2" />
<text x="50" y="60" font-family="Arial, sans-serif" font-size="24" font-weight="bold"
fill="#2E7D32" text-anchor="middle">空闲</text>
</svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref, reactive, watch } from "vue";
import { ElMessage } from "element-plus";
import { ElMessage, ElMessageBox } from "element-plus";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { updateCabinetCell } from "@/api/cabinet/cabinet-cell";
import { updateCabinetCell, clearGoodsCells } from "@/api/cabinet/cabinet-cell";
import Confirm from "@iconify-icons/ep/check";
import type { FormRules } from 'element-plus';
@ -15,6 +15,7 @@ export interface FormDTO {
cellType: number;
availableStatus: number;
usageStatus: number;
stock: number;
}
const props = defineProps({
@ -38,7 +39,8 @@ const formData = reactive<FormDTO>({
pinNo: null,
cellType: 1,
availableStatus: 1,
usageStatus: 1
usageStatus: 1,
stock: 0
});
const rules = reactive<FormRules>({
@ -86,6 +88,22 @@ const closeDialog = () => {
emit('update:modelValue', false);
};
async function handleClearGoods() {
try {
await ElMessageBox.confirm(
`确认要下架${props.row.goodsName}吗?`,
'警告',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
);
await clearGoodsCells(props.row.cellId);
ElMessage.success('商品下架成功');
emit('refresh');
closeDialog();
} catch (error) {
console.error('操作取消或失败', error);
}
}
watch(() => props.row, (val) => {
if (val) {
Object.assign(formData, val);
@ -95,18 +113,10 @@ watch(() => props.row, (val) => {
<template>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="柜体ID" prop="cabinetId">
<el-input v-model.number="formData.cabinetId" placeholder="请输入柜体ID" />
</el-form-item>
<el-form-item label="主板ID" prop="mainboardId">
<el-input v-model.number="formData.mainboardId" placeholder="请输入主板ID" />
</el-form-item>
<el-form-item label="单元格号" prop="cellNo">
<el-input v-model.number="formData.cellNo" placeholder="请输入单元格号" />
</el-form-item>
<el-form-item label="针脚号" prop="pinNo">
<el-input v-model.number="formData.pinNo" placeholder="请输入针脚号" />
</el-form-item>
@ -120,23 +130,35 @@ watch(() => props.row, (val) => {
</el-select>
</el-form-item>
<el-form-item label="使用状态" prop="usageStatus">
<el-select v-model="formData.usageStatus" placeholder="请选择状态">
<el-option label="空闲" :value="1" />
<el-option label="已占用" :value="2" />
</el-select>
<el-form-item v-if="props.row.goodsId" label="商品ID">
<el-input :model-value="props.row.goodsId" disabled />
</el-form-item>
<el-form-item v-if="props.row.goodsId" label="商品名称">
<el-input :model-value="props.row.goodsName" disabled />
</el-form-item>
<el-form-item v-if="props.row.goodsId" label="商品图片">
<el-image :src="props.row.coverImg" style="width: 100px; height: 100px" fit="contain" />
</el-form-item>
<el-form-item v-if="props.row.goodsId" label="价格">
<el-input :model-value="props.row.price" disabled />
</el-form-item>
<el-form-item v-if="props.row.goodsId" label="库存" prop="stock">
<el-input-number v-model="formData.stock" :min="0" :max="1000" class="stock-input" />
</el-form-item>
<el-form-item label="可用状态" prop="availableStatus">
<el-select v-model="formData.availableStatus" placeholder="请选择状态">
<el-option label="正常" :value="1" />
<el-option label="故障" :value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="useRenderIcon(Confirm)" @click="handleConfirm">
提交
<el-button type="primary" @click="handleConfirm" class="save-btn">
保存修改
</el-button>
<el-button v-if="props.row.goodsId" type="danger" @click="handleClearGoods" class="clear-btn">
下架商品
</el-button>
</el-form-item>
</el-form>
</template>
</template>
<style scoped lang="scss">
.save-btn, .clear-btn {
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,117 @@
<script setup lang="ts">
import { ref } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { changeGoodsCellsStock, clearGoodsCells } from "@/api/cabinet/cabinet-cell";
import { getGoodsInfo } from "@/api/shop/goods";
const props = defineProps({
cellId: {
type: Number,
required: true
},
goodsId: {
type: Number,
required: true
},
goodsName: {
type: String,
required: true
},
currentStock: {
type: Number,
required: true
},
coverImg: {
type: String,
required: true
},
price: {
type: Number,
required: true
}
});
const emit = defineEmits(['refresh', 'update:modelValue']);
const closeModal = () => {
emit('update:modelValue', false);
};
const stockInput = ref(props.currentStock);
const isEditing = ref(false);
async function handleStockChange() {
try {
const { data } = await getGoodsInfo(props.goodsId);
const remainingStock = data.stock - data.totalStock + props.currentStock;
if (stockInput.value > remainingStock) {
ElMessage.warning('分配数量不能超过剩余库存');
stockInput.value = remainingStock;
return;
}
await changeGoodsCellsStock(props.cellId, stockInput.value);
ElMessage.success('库存更新成功');
emit('refresh');
isEditing.value = false;
} catch (error) {
console.error('库存调整失败', error);
}
}
async function handleClearGoods() {
try {
await ElMessageBox.confirm(
`确认要下架${props.goodsName}吗?`,
'警告',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
);
await clearGoodsCells(props.cellId);
ElMessage.success('商品下架成功');
emit('refresh');
closeModal();
} catch (error) {
console.error('操作取消或失败', error);
}
}
</script>
<template>
<div class="config-modal">
<el-descriptions :column="1" border>
<el-descriptions-item label="商品ID" >{{ props.goodsId }}</el-descriptions-item>
<el-descriptions-item label="商品名称">{{ props.goodsName }}</el-descriptions-item>
<el-descriptions-item label="商品图片">
<el-image :src="coverImg" style="width: 100px; height: 100px" fit="contain" />
</el-descriptions-item>
<el-descriptions-item label="价格">{{ price }}</el-descriptions-item>
<el-descriptions-item label="当前库存">
<el-input-number v-model="stockInput" :min="0" :max="1000" :disabled="!isEditing" class="stock-input" />
<el-button v-if="!isEditing" type="primary" size="small" @click="isEditing = true">修改</el-button>
<el-button v-if="isEditing" type="success" size="small" @click="handleStockChange">保存</el-button>
</el-descriptions-item>
</el-descriptions>
<div class="action-buttons">
<el-button type="danger" @click="handleClearGoods">
下架商品
</el-button>
</div>
</div>
</template>
<style scoped lang="scss">
.config-modal {
padding: 20px;
}
.action-buttons {
margin-top: 20px;
display: flex;
justify-content: space-around;
}
.stock-input {
margin-right: 12px;
}
</style>

View File

@ -9,6 +9,7 @@ import GatewayConfigModal from "@/views/cabinet/smart-cabinet/GatewayConfigModal
import ShopConfigModal from "@/views/cabinet/smart-cabinet/ShopConfigModal.vue";
import MainCabinetConfigModal from "@/views/cabinet/smart-cabinet/MainCabinetConfigModal.vue";
import CabinetGoodsConfigModal from "@/views/shop/cabinet-goods/cabinet-goods-config-modal.vue";
import ConfiguredGoodsModal from "./configured-goods-modal.vue";
import { ElMessage, ElMessageBox } from "element-plus";
const { VITE_PUBLIC_IMG_PATH: IMG_PATH } = import.meta.env;
import EditPen from "@iconify-icons/ep/edit-pen";
@ -54,18 +55,24 @@ const searchCellParams = ref<CabinetCellQuery>({
goodsName: null
});
const cellPagination = ref({
pageSize: 5,
pageSize: 18,
currentPage: 1,
total: 0
});
const goodsConfigVisible = ref(false);
const configuredGoodsVisible = ref(false);
const currentCellId = ref<number>();
const cellFormVisible = ref(false);
const cellEditVisible = ref(false);
function handleConfigure(row: CabinetCellDTO) {
currentCellId.value = row.cellId;
goodsConfigVisible.value = true;
if (row.goodsId) {
currentCell.value = row;
configuredGoodsVisible.value = true;
} else {
currentCellId.value = row.cellId;
goodsConfigVisible.value = true;
}
}
async function handleStockConfig(row: CabinetCellDTO) {
@ -259,7 +266,8 @@ onMounted(() => {
<el-descriptions class="cabinet-details" :column="1" border>
<el-descriptions-item label="名称">{{ cabinetInfo.cabinetName }}</el-descriptions-item>
<el-descriptions-item label="模式">{{ getModeText(cabinetInfo.mode) }}</el-descriptions-item>
<el-descriptions-item label="格式">{{ CabinetImgMap[cabinetInfo.templateNo]?.name || '-' }}</el-descriptions-item>
<el-descriptions-item label="格式">{{ CabinetImgMap[cabinetInfo.templateNo]?.name || '-'
}}</el-descriptions-item>
<el-descriptions-item label="已用">{{ cabinetInfo.usedCells || '0' }}</el-descriptions-item>
<el-descriptions-item label="未用">{{ cabinetInfo.availableCells || '0' }}</el-descriptions-item>
<el-descriptions-item label="柜址">{{ cabinetInfo.shopName || '-' }}</el-descriptions-item>
@ -285,17 +293,17 @@ onMounted(() => {
<el-descriptions-item label="柜体名称">{{ cabinetInfo.cabinetName || '-' }}</el-descriptions-item>
<el-descriptions-item label="运行模式">{{ getModeText(cabinetInfo.mode) }}</el-descriptions-item>
<el-descriptions-item label="柜体格式">{{ CabinetImgMap[cabinetInfo.templateNo]?.name || '-'
}}</el-descriptions-item>
}}</el-descriptions-item>
<el-descriptions-item label="柜体地址">{{ cabinetInfo.shopName || '-' }}
<el-button type="success" link @click="shopConfigVisible = true">
配置
</el-button></el-descriptions-item>
<el-descriptions-item label="柜体网关">{{ cabinetInfo.mqttServerId || '-' }}
<el-button type="warning" link
@click="gatewayConfigVisible = true">
<el-button type="warning" link @click="gatewayConfigVisible = true">
配置
</el-button></el-descriptions-item>
<el-descriptions-item label="借呗支付">{{ getBalanceEnableText(cabinetInfo.balanceEnable) }}</el-descriptions-item>
<el-descriptions-item label="借呗支付">{{ getBalanceEnableText(cabinetInfo.balanceEnable)
}}</el-descriptions-item>
<!-- <el-descriptions-item label="归属类型">
{{ cabinetInfo.belongType === 0 ? '借还柜' : '固资通' }}
</el-descriptions-item> -->
@ -314,74 +322,48 @@ onMounted(() => {
搜索
</el-button>
</el-form-item>
<el-form-item>
<el-button :icon="useRenderIcon(Refresh)" @click="resetCellSearch">
重置
</el-button>
</el-form-item>
</el-form>
<!-- <el-button type="primary" :size="'small'" :icon="useRenderIcon(AddFill)" @click="cellFormVisible = true">
新增格口
</el-button> -->
</div>
<el-table v-loading="loading" :data="cellList" border>
<el-table-column label="格口ID" prop="cellId" width="80" />
<el-table-column label="格口号" prop="cellNo" width="80" />
<el-table-column label="针脚号" prop="pinNo" width="80" />
<el-table-column label="商品图片" width="120">
<template #default="{ row }">
<el-image :src="row.coverImg" :preview-src-list="[row.coverImg]" :preview-teleported="true"
:hide-on-click-modal="true" fit="cover" class="rounded" width="60" height="60" />
</template>
</el-table-column>
<el-table-column label="商品名称">
<template #default="{ row }">
{{ row.goodsId ? row.goodsName : '未配置商品' }}
</template>
</el-table-column>
<el-table-column label="价格" prop="price" width="80" />
<el-table-column label="库存" prop="stock" width="80" />
<el-table-column label="单元格类型" prop="cellType" width="120">
<template #default="{ row }">
{{ switchCellType(row.cellType) }}
</template>
</el-table-column>
<el-table-column label="购买次数" prop="orderCount" width="100" />
<el-table-column label="相关信息" width="150" fixed="right">
<template #default="{ row }">
<el-button type="success" link :icon="useRenderIcon('document')"
@click="router.push({ path: '/shop/order/index', query: { cellId: row.cellId } })">
购买记录
</el-button>
<el-button type="warning" link :icon="useRenderIcon('document')"
@click="router.push({ path: '/cabinet/operation/index', query: { cellId: row.cellId } })">
开启记录
</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button v-if="cabinetInfo.belongType === 0" type="success" link :icon="useRenderIcon(AddFill)"
@click="handleConfigure(row)">
配置商品
</el-button>
<el-button type="primary" link :icon="useRenderIcon(EditPen)" @click="handleEditCell(row)">
编辑格口
</el-button>
<el-button v-if="row.goodsId" type="warning" link :icon="useRenderIcon(EditPen)"
@click="handleStockConfig(row)">
配置库存
</el-button>
<el-button v-if="row.goodsId" type="danger" link :icon="useRenderIcon(Delete)"
@click="handleClearGoods(row)">
下架商品
</el-button>
</template>
</el-table-column>
</el-table>
<el-row :gutter="12">
<el-col v-for="(item, index) in cellList" :key="item.cellId" :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
<el-card class="cell-card" :body-style="{ padding: '8px 10px' }">
<div class="card-content">
<el-image :src="item.coverImg || `${IMG_PATH}img/product-image.svg`" class="cell-image"
fit="contain" />
<div class="cell-info">
<div class="cell-no">格口号: {{ item.cellNo }}</div>
<div class="goods-name">{{ item.goodsId ? item.goodsName : '未配置商品' }}</div>
<div class="price">价格: {{ item.price }}</div>
<div class="stock">库存: {{ item.stock }}</div>
<div class="cell-type">类型: {{ switchCellType(item.cellType) }}</div>
</div>
</div>
<div class="action-buttons">
<el-button v-if="cabinetInfo.belongType === 0 && !item.goodsId" type="success"
@click="handleConfigure(item)" class="cell-btn">
商品配置
</el-button>
<el-button v-if="cabinetInfo.belongType === 1 || item.goodsId" type="primary"
@click="handleEditCell(item)" class="cell-btn">
编辑格口
</el-button>
<!-- <el-button v-if="item.goodsId" type="warning" link :icon="useRenderIcon(EditPen)"
@click="handleStockConfig(item)">
库存
</el-button>
<el-button v-if="item.goodsId" type="danger" link :icon="useRenderIcon(Delete)"
@click="handleClearGoods(item)">
下架
</el-button> -->
</div>
</el-card>
</el-col>
</el-row>
<el-pagination v-model:current-page="cellPagination.currentPage" v-model:page-size="cellPagination.pageSize"
:page-sizes="[5, 8, 16, 24, 32]" layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[12, 18, 24, 30, 36, 42]" layout="total, sizes, prev, pager, next, jumper"
:total="cellPagination.total" @size-change="handleCellSizeChange" @current-change="handleCellPageChange"
class="pagination" />
</div>
@ -418,6 +400,11 @@ onMounted(() => {
<el-drawer v-model="goodsConfigVisible" title="配置商品" size="50%" direction="rtl">
<CabinetGoodsConfigModal v-model="goodsConfigVisible" :cell-id="currentCellId" @refresh="fetchCellList" />
</el-drawer>
<el-drawer v-model="configuredGoodsVisible" title="管理商品" size="30%" direction="rtl">
<ConfiguredGoodsModal v-model="configuredGoodsVisible" :cell-id="currentCell?.cellId"
:goods-id="currentCell?.goodsId" :goods-name="currentCell?.goodsName" :current-stock="currentCell?.stock"
:cover-img="currentCell?.coverImg" :price="currentCell?.price" @refresh="fetchCellList" />
</el-drawer>
<el-drawer v-model="cellFormVisible" title="新增格口" size="30%" direction="rtl">
<CellFormModal v-model="cellFormVisible" :initial-cabinet-id="cabinetId" @refresh="fetchCellList" />
</el-drawer>
@ -429,6 +416,65 @@ onMounted(() => {
</template>
<style scoped lang="scss">
.cell-card {
margin-bottom: 12px;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
/* &:hover {
transform: translateY(-3px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
} */
.card-content {
display: flex;
flex-direction: row;
margin: 0px;
.cell-image {
width: 50%;
height: 130px;
object-fit: contain;
border-radius: 4px;
margin-bottom: 12px;
}
.cell-info {
padding: 8px 8px 0 8px;
div {
font-size: 13px;
margin-bottom: 6px;
color: #606266;
&.goods-name {
font-weight: 500;
color: var(--el-text-color-primary);
}
&.price {
font-weight: 600;
}
}
}
}
.action-buttons {
display: flex;
justify-content: center;
margin-top: auto;
padding: 8px 0 0 0;
border-top: 1px solid var(--el-border-color-light);
.cell-btn {
margin-right: 8px;
width: 80px;
height: 30px;
}
}
}
:deep(.el-tabs__header) {
margin-bottom: 8px;
}
@ -444,6 +490,7 @@ onMounted(() => {
.el-form-item {
margin-bottom: 8px;
}
.detail-container {
display: flex;
flex-direction: column;
@ -490,6 +537,7 @@ onMounted(() => {
margin-bottom: 0px;
}
}
.pagination {
margin-top: 10px;
}