feat(shopGoods): 新增商品置顶功能及批量上下架操作

- 在商品模型中新增 isTop 字段支持置顶状态(1置顶,0不置顶)
- 商品列表新增置顶列,支持列表内联切换置顶状态
- 启用排序号列排序功能,置顶列参与前端排序
- 新增单个商品上架/下架切换接口及批量上下架操作
- 优化商品列表工具栏和选中操作栏,加入批量上架、下架按钮
- 商品分类及图片列表调用 OSS 图片压缩接口处理展示图片
- 新增删除商品导入模板及图片导入模板下载链接与使用说明
- 优化图片压缩工具,新增去除 URL 中 x-oss-process 参数功能,确保调用参数生效
This commit is contained in:
2026-07-24 10:57:23 +08:00
parent 44e416525d
commit e2f6dce7a1
11 changed files with 453 additions and 107 deletions

View File

@@ -1,89 +1,112 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
:disabled="selection?.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="openImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="openImageImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入图片</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="openDetailImport">
<template #icon>
<UploadOutlined />
</template>
<span>导入详情</span>
</a-button>
<a-button
type="dashed"
class="ele-btn-icon"
:disabled="selection?.length === 0"
@click="convertContent"
>
<template #icon>
<SyncOutlined />
</template>
<span>转换详情</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
导出xls
</a-button>
<a-radio-group v-model:value="type" @change="handleSearch">
<a-radio-button value="出售中"
>出售中({{ goodsCount?.totalNum }})
</a-radio-button>
<a-radio-button value="待上架"
>待上架({{ goodsCount?.totalNum2 }})
</a-radio-button>
<a-radio-button value="已售罄"
>已售罄({{ goodsCount?.totalNum3 }})
</a-radio-button>
</a-radio-group>
<a-tree-select
allow-clear
:tree-data="navigationList"
tree-default-expand-all
style="width: 180px"
:listHeight="700"
placeholder="请选择分类"
:value="where.categoryId || undefined"
:dropdown-style="{ overflow: 'auto' }"
@update:value="(value?: number) => (where.categoryId = value)"
@change="onCategoryId"
/>
<a-input-search
allow-clear
placeholder="请输入关键词"
style="width: 220px"
v-model:value="where.keywords"
@pressEnter="reload"
@search="reload"
/>
<a-button type="text" @click="reset">重置</a-button>
</a-space>
<div class="goods-toolbar">
<!-- 常驻工具栏 -->
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
<!-- 导入合并为下拉菜单 -->
<a-dropdown>
<a-button type="dashed" class="ele-btn-icon">
<template #icon>
<UploadOutlined />
</template>
<span>导入</span>
<DownOutlined class="ele-btn-icon-arrow" />
</a-button>
<template #overlay>
<a-menu>
<a-menu-item key="goods" @click="openImport">
导入商品
</a-menu-item>
<a-menu-item key="image" @click="openImageImport">
导入图片
</a-menu-item>
<a-menu-item key="detail" @click="openDetailImport">
导入详情
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-button type="dashed" class="ele-btn-icon" @click="handleExport">
导出xls
</a-button>
<a-radio-group v-model:value="type" @change="handleSearch">
<a-radio-button value="出售中"
>出售中({{ goodsCount?.totalNum }})
</a-radio-button>
<a-radio-button value="待上架"
>待上架({{ goodsCount?.totalNum2 }})
</a-radio-button>
<a-radio-button value="已售罄"
>已售罄({{ goodsCount?.totalNum3 }})
</a-radio-button>
</a-radio-group>
<a-tree-select
allow-clear
:tree-data="navigationList"
tree-default-expand-all
style="width: 180px"
:listHeight="700"
placeholder="请选择分类"
:value="where.categoryId || undefined"
:dropdown-style="{ overflow: 'auto' }"
@update:value="(value?: number) => (where.categoryId = value)"
@change="onCategoryId"
/>
<a-input-search
allow-clear
placeholder="请输入关键词"
style="width: 220px"
v-model:value="where.keywords"
@pressEnter="reload"
@search="reload"
/>
<a-button type="text" @click="reset">重置</a-button>
</a-space>
<!-- 选中操作栏勾选商品后才显示 -->
<div v-if="selection && selection.length > 0" class="selection-bar">
<span class="selection-bar-count">已选 {{ selection.length }} </span>
<a-button
danger
type="primary"
class="ele-btn-icon"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-button type="primary" class="ele-btn-icon" @click="batchUp">
<template #icon>
<ArrowUpOutlined />
</template>
<span>批量上架</span>
</a-button>
<a-button danger class="ele-btn-icon" @click="batchDown">
<template #icon>
<ArrowDownOutlined />
</template>
<span>批量下架</span>
</a-button>
<a-button type="dashed" class="ele-btn-icon" @click="convertContent">
<template #icon>
<SyncOutlined />
</template>
<span>转换详情</span>
</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
@@ -91,7 +114,10 @@
PlusOutlined,
DeleteOutlined,
UploadOutlined,
SyncOutlined
SyncOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DownOutlined
} from '@ant-design/icons-vue';
import { ref, watch } from 'vue';
import { getCount } from '@/api/shop/shopGoods';
@@ -102,8 +128,8 @@
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
// 选中的商品
selection?: any[];
merchantId?: number;
navigationList?: ShopGoodsCategory[];
}>(),
@@ -131,6 +157,8 @@
(e: 'search', where?: ShopGoodsParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchUp'): void;
(e: 'batchDown'): void;
(e: 'batchMove'): void;
(e: 'import'): void;
(e: 'imageImport'): void;
@@ -149,6 +177,16 @@
emit('remove');
};
// 批量上架
const batchUp = () => {
emit('batchUp');
};
// 批量下架
const batchDown = () => {
emit('batchDown');
};
// 打开导入
const openImport = () => {
emit('import');
@@ -220,3 +258,35 @@
{ immediate: true }
);
</script>
<style lang="less" scoped>
.goods-toolbar {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 选中后浮现的操作栏 */
.selection-bar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding: 10px 14px;
background: #e6f4ff;
border: 1px solid #91caff;
border-radius: 6px;
}
.selection-bar-count {
font-weight: 600;
color: #1677ff;
margin-right: 4px;
white-space: nowrap;
}
.ele-btn-icon-arrow {
font-size: 12px;
margin-left: 2px;
}
</style>

View File

@@ -20,16 +20,53 @@
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传 xlsxlsx 文件</span>
<span>只能上传 xlsxlsx 文件</span>
<a :href="templateUrl" download="商品详情导入模板.xlsx">下载导入模板</a>
</div>
<!-- 使用说明 -->
<a-alert type="info" show-icon :style="{ marginTop: '16px' }">
<template #message>
<div class="import-tips">
<div class="import-tips-title">使用说明</div>
<ul class="import-tips-list">
<li>Excel 中每行对应一个商品的详情内容</li>
<li>图片按<strong>文件夹</strong>组织<strong>文件夹名称为商品编号</strong></li>
<li>文件夹内的图片按<strong>数字序号</strong>命名排序 1.jpg2.png3.jpg </li>
<li>图片序号对应 Excel 中的图片展示顺序</li>
</ul>
</div>
</template>
</a-alert>
</ele-modal>
</template>
<style lang="less" scoped>
.import-tips {
font-size: 13px;
line-height: 1.8;
color: #555;
}
.import-tips-title {
font-weight: 600;
margin-bottom: 4px;
color: #333;
}
.import-tips-list {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 2px;
}
}
</style>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importShopGoodsDetails } from '@/api/shop/shopGoods';
import { MODULES_API_URL } from '@/config/setting';
defineProps<{
visible: boolean;
@@ -42,6 +79,10 @@
const loading = ref(false);
const templateUrl = computed(() => {
return MODULES_API_URL + '/shop/shop-goods/import/details/template';
});
const doUpload = ({ file }) => {
if (
![

View File

@@ -20,16 +20,18 @@
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传 zip 文件图片文件名需与商品编号对应</span>
<span>只能上传 zip 文件图片文件名需与商品编号对应</span>
<a :href="templateUrl" download="商品图片导入示例.zip">下载导入模板</a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importShopGoodsImages } from '@/api/shop/shopGoods';
import { MODULES_API_URL } from '@/config/setting';
defineProps<{
visible: boolean;
@@ -42,6 +44,10 @@
const loading = ref(false);
const templateUrl = computed(() => {
return MODULES_API_URL + '/shop/shop-goods/import/images/template';
});
const doUpload = ({ file }) => {
const isZip =
file.type === 'application/zip' ||