shop-web/src/pages/manage/goods/goodsList.vue

261 lines
7.2 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue';
import { showConfirmDialog, showDialog, showSuccessToast } from 'vant';
import {
getGoodsList,
addGoods,
deleteGoods,
updateGoods,
type ShopGoodsDTO,
type SearchShopGoodsQuery
} from '@/common/apis/manage/goods';
// 商品列表数据
const goodsList = ref<ShopGoodsDTO[]>([]);
const loading = ref(false);
const finished = ref(false);
// 搜索参数
const searchParams = reactive<SearchShopGoodsQuery>({
pageNum: 1,
pageSize: 10,
goodsName: '',
status: undefined
});
// 弹窗控制
const showEditDialog = ref(false);
const currentGoods = ref<Partial<ShopGoodsDTO>>({});
const isEditMode = ref(false);
// 获取商品列表
const fetchGoodsList = async () => {
try {
const res = await getGoodsList(searchParams);
goodsList.value = res.data.rows;
} catch (e) {
showDialog({ message: '加载失败,请重试' });
}
};
// 加载数据
const onLoad = async () => {
try {
const res = await getGoodsList(searchParams);
goodsList.value.push(...res.data.rows);
loading.value = false;
if (goodsList.value.length >= res.data.total) {
finished.value = true;
} else {
if (searchParams.pageNum) {
searchParams.pageNum++;
} else {
searchParams.pageNum = 1;
}
}
} catch (error) {
console.error('获取商品列表失败', error);
finished.value = true;
}
};
// 删除商品
const handleDelete = async (ids: number[]) => {
await showConfirmDialog({ message: '确认删除选中商品?' });
await deleteGoods(ids);
showSuccessToast('删除成功');
await fetchGoodsList();
};
// 提交表单
const submitForm = async () => {
try {
if (isEditMode.value) {
await updateGoods(currentGoods.value.goodsId!, currentGoods.value);
} else {
await addGoods({
goodsName: currentGoods.value.goodsName || '',
categoryId: currentGoods.value.categoryId || 0,
price: currentGoods.value.price || 0,
stock: currentGoods.value.stock || 0,
status: currentGoods.value.status || 1,
autoApproval: currentGoods.value.autoApproval || 0,
coverImg: currentGoods.value.coverImg || '',
usageInstruction: currentGoods.value.usageInstruction
});
}
showSuccessToast('操作成功');
showEditDialog.value = false;
await fetchGoodsList();
} catch (e) {
showDialog({ message: '操作失败' });
}
};
// 打开编辑弹窗
const openEdit = (goods?: ShopGoodsDTO) => {
currentGoods.value = goods ? { ...goods } : { status: 1 };
isEditMode.value = !!goods;
showEditDialog.value = true;
};
onMounted(fetchGoodsList);
</script>
<template>
<div class="goods-manage">
<!-- 搜索栏和操作按钮 -->
<div class="search-action-bar">
<van-search v-model="searchParams.goodsName" placeholder="输入商品名称搜索" @search="fetchGoodsList" />
<van-button type="primary" @click="openEdit">添加商品</van-button>
</div>
<!-- 商品表格 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div class="goods-grid">
<van-cell v-for="item in goodsList" :key="item.goodsId" class="goods-card">
<template #icon>
<van-image :src="item.coverImg" width="80" height="80" class="goods-image">
<div v-if="item.stock === 0" class="sold-out-overlay">
<span class="sold-out-text">已售罄</span>
</div>
<template #error>
<div class="custom-error">
图片加载失败
</div>
</template>
</van-image>
</template>
<div class="goods-info">
<div class="goods-name van-ellipsis">{{ item.goodsName }}</div>
<div class="goods-price">¥{{ item.price?.toFixed(2) }}</div>
<div class="action-row">
<span v-if="item.stock! > 0" class="stock-count">库存: {{ item.stock }}</span>
<div class="goods-actions">
<van-button size="mini" @click="openEdit(item)">编辑</van-button>
</div>
</div>
</div>
</van-cell>
</div>
</van-list>
<!-- 编辑弹窗 -->
<van-dialog v-model:show="showEditDialog" :title="isEditMode ? '编辑商品' : '新增商品'">
<van-form @submit="submitForm">
<van-cell-group inset>
<van-field v-model="currentGoods.goodsName" label="商品名称" placeholder="请输入"
:rules="[{ required: true }]" />
<van-field v-model="currentGoods.price" label="价格" type="number" placeholder="请输入" />
<van-field v-model="currentGoods.stock" label="库存" type="number" placeholder="请输入" />
</van-cell-group>
<div style="padding: 16px">
<van-button block type="primary" native-type="submit">提交</van-button>
</div>
</van-form>
</van-dialog>
</div>
</template>
<style scoped>
.goods-manage {
padding: 4px;
}
.search-action-bar {
margin: 0;
display: flex;
align-items: center;
gap: 4px;
}
.search-action-bar .van-search {
flex: 1;
}
.goods-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 4px;
margin: 4px 0;
}
.goods-card {
margin-bottom: 10px;
padding: min(2.667vw, 20px) 0;
}
.goods-image {
margin-right: 12px;
border-radius: 4px;
overflow: hidden;
}
.goods-info {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 80px;
position: relative;
}
.goods-name {
font-size: 14px;
color: #333;
line-height: 1.4;
text-align: left;
}
.goods-price {
font-size: 16px;
color: #e95d5d;
font-weight: bold;
text-align: left;
}
.action-row {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-top: auto;
}
.stock-count {
font-size: 11px;
color: #bbbbbb;
margin-right: 8px;
display: flex;
align-items: flex-end;
height: 100%;
line-height: 1;
}
.sold-out-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
}
.sold-out-text {
color: #999;
font-size: 14px;
transform: rotate(-15deg);
border: 1px solid #eee;
padding: 2px 8px;
border-radius: 4px;
}
.custom-error {
color: #999;
font-size: 12px;
text-align: center;
padding: 10px;
}
</style>