shop-front-end/src/views/shop/goods/index.vue

503 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref } from "vue";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { getGoodsListApi, GoodsDTO } from "@/api/shop/goods";
import EditPen from "@iconify-icons/ep/edit-pen";
import Delete from "@iconify-icons/ep/delete";
import AddFill from "@iconify-icons/ri/add-circle-line";
import Search from "@iconify-icons/ep/search";
import Refresh from "@iconify-icons/ep/refresh";
import Setting from "@iconify-icons/ep/setting";
import View from "@iconify-icons/ep/view";
import Picture from "@iconify-icons/ep/picture";
import Loading from "@iconify-icons/ep/loading";
import GoodsFormModal from "./goods-form-modal.vue";
import GoodsEditModal from "./goods-edit-modal.vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { deleteGoodsApi } from "@/api/shop/goods";
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
import { useRouter } from "vue-router";
import { useWxStore } from "@/store/modules/wx";
import { useBtnPermissionStore } from "@/store/modules/btnPermission";
import {
compressAllGoodsImages,
previewCompressResults,
type CompressProgress,
type CompressResult
} from "@/utils/goodsImageCompressor";
import { ImageQuality } from "@/utils/imageCompressor";
import { defineProps } from "vue";
defineOptions({
name: "ShopGoods"
});
const wxStore = useWxStore();
const router = useRouter();
const { hasPermission } = useBtnPermissionStore();
const formRef = ref();
const tableRef = ref();
const modalVisible = ref(false);
// 搜索表单
const searchFormParams = ref({
corpid: wxStore.corpid,
belongType: null,
goodsName: "",
status: null
});
// 分页参数
const pagination = ref({
pageSize: 12,
currentPage: 1,
total: 0
});
// 加载数据
const loading = ref(false);
const dataList = ref<GoodsDTO[]>([]);
const multipleSelection = ref<number[]>([]);
const editVisible = ref(false);
const currentRow = ref<GoodsDTO>();
// 压缩相关状态
const compressLoading = ref(false);
const compressProgress = ref<CompressProgress | null>(null);
const compressResults = ref<CompressResult[]>([]);
const getList = async () => {
try {
loading.value = true;
const { data } = await getGoodsListApi({
...searchFormParams.value,
corpid: wxStore.corpid,
pageSize: pagination.value.pageSize,
pageNum: pagination.value.currentPage
});
dataList.value = data.rows;
pagination.value.total = data.total;
} finally {
loading.value = false;
}
};
const handleAddSuccess = () => {
getList();
};
// 搜索
const onSearch = () => {
pagination.value.currentPage = 1;
getList();
};
// 重置
const resetForm = () => {
formRef.value.resetFields();
onSearch();
};
// 分页变化
const onSizeChange = (val: number) => {
pagination.value.pageSize = val;
getList();
};
const onCurrentChange = (val: number) => {
pagination.value.currentPage = val;
getList();
};
// 初始化加载
getList();
const handleDelete = async (row: any) => {
try {
await deleteGoodsApi(row.goodsId);
ElMessage.success("删除成功");
getList();
} catch (error) {
console.error("删除失败", error);
}
};
const handleBulkDelete = async () => {
if (multipleSelection.value.length === 0) return;
try {
await ElMessageBox.confirm(
`确认删除选中的${multipleSelection.value.length}项商品吗?`,
"警告",
{ confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
);
await deleteGoodsApi(multipleSelection.value.join(","));
ElMessage.success("批量删除成功");
multipleSelection.value = [];
getList();
} catch (error) {
console.error("删除取消或失败", error);
}
};
// 在表格绑定选择事件
const handleSelectionChange = (rows: any[]) => {
multipleSelection.value = rows.map(row => row.goodsId);
};
const handleEdit = (row: GoodsDTO) => {
currentRow.value = row;
editVisible.value = true;
};
const handleViewDetail = (row: GoodsDTO) => {
// 保存信息到标签页
useMultiTagsStoreHook().handleTags("push", {
path: `/shop/goods/detail`,
name: "GoodsDetail",
query: { id: row.goodsId },
meta: {
title: `${row.goodsName}`,
dynamicLevel: 3
}
});
router.push({
path: '/shop/goods/detail',
query: {
id: row.goodsId
}
});
};
// 一键压缩所有商品图片
const handleCompressAllImages = async () => {
try {
await ElMessageBox.confirm(
"此操作将对所有商品的封面图进行压缩和重新上传,可能需要较长时间,是否继续?",
"提示",
{
confirmButtonText: "确定压缩",
cancelButtonText: "取消",
type: "warning"
}
);
compressLoading.value = true;
compressProgress.value = {
total: 0,
current: 0,
success: 0,
failed: 0,
skipped: 0
};
const results = await compressAllGoodsImages({
quality: ImageQuality.MEDIUM,
maxWidth: 980,
maxHeight: 980,
onProgress: (progress) => {
compressProgress.value = progress;
}
});
compressResults.value = results;
compressLoading.value = false;
// 显示结果
const success = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
if (failed === 0) {
ElMessage.success(`所有商品图片压缩完成!成功: ${success}`);
} else {
ElMessage.warning(
`压缩完成!成功: ${success}个,失败: ${failed}`
);
}
// 打印详细结果
previewCompressResults(results);
// 刷新列表
getList();
} catch (error) {
compressLoading.value = false;
if (error !== "cancel") {
console.error("压缩失败", error);
ElMessage.error("压缩失败,请查看控制台错误信息");
}
}
};
</script>
<template>
<div class="main">
<el-form ref="formRef" :inline="true" :model="searchFormParams"
class="search-form bg-bg_color flex w-[99/100] pl-[22px] pt-[12px]">
<el-form-item prop="goodsName">
<el-input @keydown.enter.prevent="onSearch" v-model="searchFormParams.goodsName" placeholder="请输入商品名称" clearable
class="!w-[200px]" />
</el-form-item>
<el-form-item prop="belongType">
<el-select v-model="searchFormParams.belongType" placeholder="请选择商品所属" clearable @change="onSearch"
class="!w-[180px]">
<el-option label="智柜通" :value="0" />
<el-option label="固资通" :value="1" />
</el-select>
</el-form-item>
<!-- <el-form-item label="状态:" prop="status">
<el-select v-model="searchFormParams.status" placeholder="请选择状态" clearable class="!w-[180px]">
<el-option label="已上架" :value="1" />
<el-option label="已下架" :value="2" />
</el-select>
</el-form-item> -->
<el-form-item>
<el-button type="primary" :icon="useRenderIcon(Search)" @click="onSearch">
搜索
</el-button>
</el-form-item>
<el-form-item class="space-item">
</el-form-item>
<el-form-item>
<el-button v-if="hasPermission('shop:goods:write')" type="primary" :icon="useRenderIcon(AddFill)" @click="modalVisible = true"
style="margin-right: 10px;">
新增商品
</el-button>
<!-- <el-button v-if="hasPermission('shop:goods:write')" type="warning" :icon="useRenderIcon(Picture)" @click="handleCompressAllImages"
:loading="compressLoading" style="margin-right: 10px;">
{{ compressLoading ? '压缩中...' : '一键压缩图片' }}
</el-button> -->
</el-form-item>
</el-form>
<div class="grid-container">
<el-row :gutter="12">
<el-col v-for="(item, index) in dataList" :key="item.goodsId" :xs="24" :sm="12" :md="8" :lg="4" :xl="4">
<el-card class="goods-card" :body-style="{ padding: '8px 10px' }">
<div class="card-content">
<el-image :src="item.coverImg" :preview-src-list="[item.coverImg]" class="goods-image" fit="contain" />
<div class="goods-info">
<div class="price">价格{{ item.price }}</div>
<div class="stock">库存{{ item.stock }}</div>
<div class="info-item">状态{{ item.status === 1 ? '已上架' : '已下架' }}</div>
</div>
</div>
<div class="detail-btn" @click="handleViewDetail(item)">
{{ item.goodsName }}
</div>
</el-card>
</el-col>
</el-row>
<el-pagination v-model:current-page="pagination.currentPage" v-model:page-size="pagination.pageSize"
:page-sizes="[12, 18, 24, 30, 36, 42]" layout="total, sizes, prev, pager, next, jumper"
:total="pagination.total" @size-change="onSizeChange" @current-change="onCurrentChange" class="pagination" />
</div>
<!-- 压缩进度显示 -->
<!-- <div v-if="compressLoading || compressProgress" class="compress-progress">
<el-card class="progress-card">
<template #header>
<div class="card-header">
<span>图片压缩进度</span>
</div>
</template>
<div v-if="compressProgress">
<div class="progress-info">
<p>总商品数:{{ compressProgress.total }}</p>
<p>当前处理:{{ compressProgress.current }} / {{ compressProgress.total }}</p>
<p>成功:<span class="success">{{ compressProgress.success }}</span></p>
<p>失败:<span class="failed">{{ compressProgress.failed }}</span></p>
<p>跳过:<span class="skipped">{{ compressProgress.skipped }}</span></p>
</div>
<el-progress
:percentage="Math.round((compressProgress.current / compressProgress.total) * 100)"
:status="compressProgress.failed > 0 ? 'exception' : 'success'"
/>
</div>
<div v-else class="waiting">
<el-icon class="is-loading">
<component :is="useRenderIcon(Loading)" />
</el-icon>
<p>准备中...</p>
</div>
</el-card>
</div> -->
<!-- 新增商品弹窗 -->
<goods-form-modal v-model:visible="modalVisible" @refresh="getList" />
<goods-edit-modal v-model:visible="editVisible" :row="currentRow" @refresh="getList" />
</div>
</template>
<style scoped lang="scss">
/* 覆盖预览层样式 */
:deep(.el-image-viewer__wrapper) {
z-index: 9999 !important;
}
:deep(.el-image-viewer__mask) {
opacity: 1;
background-color: rgba(0, 0, 0, 0.8);
}
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
margin-right: 12px;
}
}
.space-item {
flex: 1;
width: 100%;
}
.pagination {
margin-top: 10px;
text-align: center;
:deep(.el-pagination) {
margin: 0;
padding: 4px 0;
}
}
.goods-card {
margin-bottom: 12px;
min-height: 210px;
display: flex;
flex-direction: column;
justify-content: space-between;
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;
.goods-image {
width: 55%;
height: 180px;
object-fit: contain;
border-radius: 4px;
margin-right: 10px;
}
.goods-info {
flex: 1;
padding: 16px 0 0 0;
.name,
.info-item,
.price,
.stock {
font-size: 14px;
font-weight: 500;
margin-bottom: 6px;
color: #606266;
}
/* .price,
.info-item,
.stock {
color: #909399;
} */
}
}
.divider {
margin: 5px 0px;
}
.detail-btn {
&:hover {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
transition: all 0.3s ease;
}
width: 100%;
margin-top: auto;
padding: 4px 0;
text-align: center;
color: #409eff;
font-weight: 500;
cursor: pointer;
}
.grid-container {
margin-top: 8px;
background-color: var(--el-bg-color);
border-radius: 4px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
margin-bottom: 12px;
min-height: 300px;
.el-row {
margin-bottom: -12px;
}
}
.compress-progress {
margin-top: 12px;
.progress-card {
background-color: var(--el-bg-color);
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.progress-info {
margin-bottom: 16px;
p {
margin: 8px 0;
font-size: 14px;
color: var(--el-text-color-regular);
.success {
color: var(--el-color-success);
font-weight: 600;
}
.failed {
color: var(--el-color-danger);
font-weight: 600;
}
.skipped {
color: var(--el-color-warning);
font-weight: 600;
}
}
}
.waiting {
text-align: center;
padding: 20px 0;
.el-icon {
font-size: 32px;
color: var(--el-color-primary);
}
p {
margin-top: 12px;
color: var(--el-text-color-regular);
}
}
}
}
</style>