261 lines
7.2 KiB
Vue
261 lines
7.2 KiB
Vue
|
<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>
|