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

@@ -0,0 +1,15 @@
# 2026-07-24 工作日志
## shopGoods 商品列表排序机制分析
- 现状:列表「排序号」列(`src/views/shop/shopGoods/index.vue` 约 273-279 行)只做展示,**未启用 `sorter`**,列表默认也不按 `sortNumber` 排序(默认排序由后端决定,前端 `orders` 仅来自「创建时间」列的排序)。
- 编辑表单里「排序号」是 `a-input-number`0-9999默认 100且在 `rules` 里是 **必填**
- 已存在类似的布尔开关 `recommend`列表内联切换index.vue 约 47-58 行),「置顶」可复用同一 UI 模式。
- 结论倾向:纯排序号在商品多时难用,建议「置顶开关 + 少量置顶项内排序 + 其余按默认(创建时间/销量)」的混合方案,而非完全替换。
## 已实现:方案 B商品置顶功能
- 数据模型 `src/api/shop/shopGoods/model/index.ts`:新增 `isTop?: number`(置顶 1/不置顶 0
- 列表 `src/views/shop/shopGoods/index.vue`
- 新增「置顶」列,列表内联一键切换(复用 recommend 的 CheckOutlined/CloseOutlined 模式),新增 `onTop` 处理函数。
- 「排序号」列开启 `sorter: true`(之前只展示、不可排序)。
- 后端迁移脚本 `add-is-top-shop-goods.sql`:新增 `is_top` TINYINT 字段;分页默认排序需改为 `ORDER BY is_top DESC, sort_number ASC, create_time DESC`(后端不在本仓库,需手动改 page 接口 ORDER BY / 放开 orders 白名单)。
- 注意:置顶真正"排到最前"依赖后端默认排序改动;点击列排序也需后端放开 is_top / sort_number。

View File

@@ -21,3 +21,12 @@
- `2` = 商家送货 - `2` = 商家送货
来源:管理后台 `src/views/shop/shopOrder/components/deliveryModal.vue` 来源:管理后台 `src/views/shop/shopOrder/components/deliveryModal.vue`
### shopGoods status 字段(上架/下架)
- `0` = 已上架 / 上架
- `1` = 待上架 / 下架
- `2` = 待审核,`3` = 审核不通过
- 编辑表单「状态」radio 绑定 `form.status`0=上架、1=下架。
- 一键上下架、批量上下架均通过 `updateShopGoods({ ...record, status })` 直接改 status 实现(不额外用 isShow 字段)。
- 列表 tag 仍显示0=已上架、1=待上架、2=待审核、3=审核不通过。

35
add-is-top-shop-goods.sql Normal file
View File

@@ -0,0 +1,35 @@
-- ============================================================
-- 商品表新增「置顶」字段 is_top并调整默认排序方案 B
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin
-- 1. is_top = 1 表示置顶0默认表示不置顶
-- 2. 表名/字段名 snake_case 约定Node + TypeORM 常见);
-- 若你的库列名是驼峰goodsId / isTop请自行替换
-- 3. 与 recommend推荐同为「列表内联一键切换」的布尔开关
-- 4. 混合排序思路:置顶的永远在最前;置顶内部按排序号升序;
-- 其余商品按创建时间倒序(绝大多数商品无需手动排)
-- ============================================================
-- 第一步新增字段已存在则跳过MySQL 5.7 / 8 通用)
ALTER TABLE shop_goods
ADD COLUMN is_top TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶1置顶 0不置顶';
-- (可选)联合索引,置顶 + 排序号分页查询更稳
-- ALTER TABLE shop_goods
-- ADD INDEX idx_is_top_sort (is_top, sort_number);
-- 第二步:调整分页查询(page 接口)默认排序
-- 原示例ORDER BY create_time DESC
-- 改: ORDER BY is_top DESC, sort_number ASC, create_time DESC
-- 说明:若后端排序字段来自前端 orders 参数,需放开 is_top / sort_number 参与排序
-- (前端「置顶」「排序号」列已开启 sorter会向后端传 orders
-- 第三步(验证):确认字段已加、数据默认 0且排序符合预期
SELECT goods_id,
name,
is_top,
sort_number,
create_time
FROM shop_goods
ORDER BY is_top DESC, sort_number ASC, create_time DESC
LIMIT 20;

View File

@@ -0,0 +1,37 @@
-- ============================================================
-- 下架「分类ID = 10332 且 没有图片」的商品
-- ------------------------------------------------------------
-- 约定说明(依据本项目代码 /Users/gxwebsoft/VUE/guilixu-admin
-- 1. 下架 = status = 1项目约定0 上架 / 1 下架,见 shopGoodsEdit.vue 状态 radio
-- 2. 「没有图片」以封面图 image 是否为空判断(封面图为必填项)
-- 3. 已删除商品deleted = 1不参与避免误改
-- 4. 表名/字段名按 snake_case 约定Node + TypeORM 常见);
-- 若你的库列名是驼峰goodsId / categoryId请自行替换
-- 5. 分类 10332 若是父分类、需连同其子分类商品一起处理,
-- 请把 category_id = 10332 改为 category_id IN (10332, 子分类ID...)
-- ============================================================
-- 第一步:先预览,确认影响范围(务必先执行核对,再跑 UPDATE
SELECT goods_id,
name,
image,
status
FROM shop_goods
WHERE category_id = 10332
AND (image IS NULL OR image = '')
AND (deleted = 0 OR deleted IS NULL);
-- 第二步:确认无误后执行下架
UPDATE shop_goods
SET status = 1,
update_time = NOW()
WHERE category_id = 10332
AND (image IS NULL OR image = '')
AND (deleted = 0 OR deleted IS NULL);
-- ------------------------------------------------------------
-- (可选)若你认为「没有图片」应同时要求轮播图也为空,
-- 即封面图和轮播图都没有才算没图,使用下面更严格的条件:
-- AND (image IS NULL OR image = '')
-- AND (files IS NULL OR files = '' OR files = '[]')
-- ------------------------------------------------------------

View File

@@ -87,6 +87,8 @@ export interface ShopGoods {
gainIntegral?: number; gainIntegral?: number;
// 推荐 // 推荐
recommend?: number; recommend?: number;
// 置顶1置顶 0不置顶
isTop?: number;
// 商户ID // 商户ID
merchantId?: number; merchantId?: number;
// 商户名称 // 商户名称

View File

@@ -41,6 +41,26 @@ export function ensureFullUrl(url: string): string {
return `${OSS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`; return `${OSS_BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`;
} }
/**
* 移除 URL 中已有的 x-oss-process 参数(无论其在 query 中的位置)
*
* 阿里云 OSS 的图片处理参数通常以 `x-oss-process=image/...` 形式附加在 query 中,
* 可能位于首个或后续参数位。本函数精确剔除该参数,保留其余 query 参数。
*
* @param url 完整图片 URL
* @returns 已剥离 x-oss-process 的 URL
*/
function removeOssProcess(url: string): string {
const qIndex = url.indexOf('?');
if (qIndex === -1) return url;
const base = url.slice(0, qIndex);
const params = url
.slice(qIndex + 1)
.split('&')
.filter((p) => !p.startsWith('x-oss-process='));
return params.length ? `${base}?${params.join('&')}` : base;
}
/** /**
* 获取 OSS 压缩后的图片 URL * 获取 OSS 压缩后的图片 URL
* *
@@ -49,8 +69,11 @@ export function ensureFullUrl(url: string): string {
* *
* 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。 * 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。
* *
* 优先级:以调用方指定的 width/quality 为准。若 URL 已自带 x-oss-process
* (接口已返回压缩参数),会先剥离再按本函数参数重新拼接,确保调用方参数生效。
*
* @param url 原始图片 URL * @param url 原始图片 URL
* @param options 压缩选项 * @param options 压缩选项width/quality 不传则使用默认值)
* @returns 处理后的图片 URL * @returns 处理后的图片 URL
*/ */
export function getCompressedImageUrl( export function getCompressedImageUrl(
@@ -66,11 +89,12 @@ export function getCompressedImageUrl(
// 未启用压缩,原样返回 // 未启用压缩,原样返回
if (!enabled) return fullUrl; if (!enabled) return fullUrl;
// 已包含 OSS 处理参数,避免重复拼接 // 优先使用调用方指定的 width/quality
if (fullUrl.includes('x-oss-process')) return fullUrl; // 先剥离 URL 自带的 x-oss-process再按本函数参数重新拼接确保以调用方为准。
const baseUrl = removeOssProcess(fullUrl);
// 已有其他 query 参数用 & 拼接,否则用 ? // 已有其他 query 参数用 & 拼接,否则用 ?
const separator = fullUrl.includes('?') ? '&' : '?'; const separator = baseUrl.includes('?') ? '&' : '?';
return `${fullUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`; return `${baseUrl}${separator}x-oss-process=image/resize,w_${width}/quality,Q_${quality}`;
} }

View File

@@ -1,89 +1,112 @@
<!-- 搜索表单 --> <!-- 搜索表单 -->
<template> <template>
<a-space :size="10" style="flex-wrap: wrap"> <div class="goods-toolbar">
<a-button type="primary" class="ele-btn-icon" @click="add"> <!-- 常驻工具栏 -->
<template #icon> <a-space :size="10" style="flex-wrap: wrap">
<PlusOutlined /> <a-button type="primary" class="ele-btn-icon" @click="add">
</template> <template #icon>
<span>添加</span> <PlusOutlined />
</a-button> </template>
<a-button <span>添加</span>
danger </a-button>
type="primary"
class="ele-btn-icon" <!-- 导入合并为下拉菜单 -->
:disabled="selection?.length === 0" <a-dropdown>
@click="removeBatch" <a-button type="dashed" class="ele-btn-icon">
> <template #icon>
<template #icon> <UploadOutlined />
<DeleteOutlined /> </template>
</template> <span>导入</span>
<span>批量删除</span> <DownOutlined class="ele-btn-icon-arrow" />
</a-button> </a-button>
<a-button type="dashed" class="ele-btn-icon" @click="openImport"> <template #overlay>
<template #icon> <a-menu>
<UploadOutlined /> <a-menu-item key="goods" @click="openImport">
</template> 导入商品
<span>导入</span> </a-menu-item>
</a-button> <a-menu-item key="image" @click="openImageImport">
<a-button type="dashed" class="ele-btn-icon" @click="openImageImport"> 导入图片
<template #icon> </a-menu-item>
<UploadOutlined /> <a-menu-item key="detail" @click="openDetailImport">
</template> 导入详情
<span>导入图片</span> </a-menu-item>
</a-button> </a-menu>
<a-button type="dashed" class="ele-btn-icon" @click="openDetailImport"> </template>
<template #icon> </a-dropdown>
<UploadOutlined />
</template> <a-button type="dashed" class="ele-btn-icon" @click="handleExport">
<span>导入详情</span> 导出xls
</a-button> </a-button>
<a-button
type="dashed" <a-radio-group v-model:value="type" @change="handleSearch">
class="ele-btn-icon" <a-radio-button value="出售中"
:disabled="selection?.length === 0" >出售中({{ goodsCount?.totalNum }})
@click="convertContent" </a-radio-button>
> <a-radio-button value="待上架"
<template #icon> >待上架({{ goodsCount?.totalNum2 }})
<SyncOutlined /> </a-radio-button>
</template> <a-radio-button value="已售罄"
<span>转换详情</span> >已售罄({{ goodsCount?.totalNum3 }})
</a-button> </a-radio-button>
<a-button type="dashed" class="ele-btn-icon" @click="handleExport"> </a-radio-group>
导出xls
</a-button> <a-tree-select
<a-radio-group v-model:value="type" @change="handleSearch"> allow-clear
<a-radio-button value="出售中" :tree-data="navigationList"
>出售中({{ goodsCount?.totalNum }}) tree-default-expand-all
</a-radio-button> style="width: 180px"
<a-radio-button value="待上架" :listHeight="700"
>待上架({{ goodsCount?.totalNum2 }}) placeholder="请选择分类"
</a-radio-button> :value="where.categoryId || undefined"
<a-radio-button value="已售罄" :dropdown-style="{ overflow: 'auto' }"
>已售罄({{ goodsCount?.totalNum3 }}) @update:value="(value?: number) => (where.categoryId = value)"
</a-radio-button> @change="onCategoryId"
</a-radio-group> />
<a-tree-select <a-input-search
allow-clear allow-clear
:tree-data="navigationList" placeholder="请输入关键词"
tree-default-expand-all style="width: 220px"
style="width: 180px" v-model:value="where.keywords"
:listHeight="700" @pressEnter="reload"
placeholder="请选择分类" @search="reload"
:value="where.categoryId || undefined" />
:dropdown-style="{ overflow: 'auto' }" <a-button type="text" @click="reset">重置</a-button>
@update:value="(value?: number) => (where.categoryId = value)" </a-space>
@change="onCategoryId"
/> <!-- 选中操作栏勾选商品后才显示 -->
<a-input-search <div v-if="selection && selection.length > 0" class="selection-bar">
allow-clear <span class="selection-bar-count">已选 {{ selection.length }} </span>
placeholder="请输入关键词" <a-button
style="width: 220px" danger
v-model:value="where.keywords" type="primary"
@pressEnter="reload" class="ele-btn-icon"
@search="reload" @click="removeBatch"
/> >
<a-button type="text" @click="reset">重置</a-button> <template #icon>
</a-space> <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> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -91,7 +114,10 @@
PlusOutlined, PlusOutlined,
DeleteOutlined, DeleteOutlined,
UploadOutlined, UploadOutlined,
SyncOutlined SyncOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DownOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { getCount } from '@/api/shop/shopGoods'; import { getCount } from '@/api/shop/shopGoods';
@@ -102,8 +128,8 @@
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
// 选中的角色 // 选中的商品
selection?: []; selection?: any[];
merchantId?: number; merchantId?: number;
navigationList?: ShopGoodsCategory[]; navigationList?: ShopGoodsCategory[];
}>(), }>(),
@@ -131,6 +157,8 @@
(e: 'search', where?: ShopGoodsParam): void; (e: 'search', where?: ShopGoodsParam): void;
(e: 'add'): void; (e: 'add'): void;
(e: 'remove'): void; (e: 'remove'): void;
(e: 'batchUp'): void;
(e: 'batchDown'): void;
(e: 'batchMove'): void; (e: 'batchMove'): void;
(e: 'import'): void; (e: 'import'): void;
(e: 'imageImport'): void; (e: 'imageImport'): void;
@@ -149,6 +177,16 @@
emit('remove'); emit('remove');
}; };
// 批量上架
const batchUp = () => {
emit('batchUp');
};
// 批量下架
const batchDown = () => {
emit('batchDown');
};
// 打开导入 // 打开导入
const openImport = () => { const openImport = () => {
emit('import'); emit('import');
@@ -220,3 +258,35 @@
{ immediate: true } { immediate: true }
); );
</script> </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-upload-dragger>
</a-spin> </a-spin>
<div class="ele-text-center"> <div class="ele-text-center">
<span>只能上传 xlsxlsx 文件</span> <span>只能上传 xlsxlsx 文件</span>
<a :href="templateUrl" download="商品详情导入模板.xlsx">下载导入模板</a>
</div> </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> </ele-modal>
</template> </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> <script lang="ts" setup>
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { message } from 'ant-design-vue/es'; import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue'; import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importShopGoodsDetails } from '@/api/shop/shopGoods'; import { importShopGoodsDetails } from '@/api/shop/shopGoods';
import { MODULES_API_URL } from '@/config/setting';
defineProps<{ defineProps<{
visible: boolean; visible: boolean;
@@ -42,6 +79,10 @@
const loading = ref(false); const loading = ref(false);
const templateUrl = computed(() => {
return MODULES_API_URL + '/shop/shop-goods/import/details/template';
});
const doUpload = ({ file }) => { const doUpload = ({ file }) => {
if ( if (
![ ![

View File

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

View File

@@ -10,6 +10,7 @@
:columns="columns" :columns="columns"
:datasource="datasource" :datasource="datasource"
:customRow="customRow" :customRow="customRow"
size="small"
v-model:selection="selection" v-model:selection="selection"
tool-class="ele-toolbar-form" tool-class="ele-toolbar-form"
class="sys-org-table" class="sys-org-table"
@@ -21,6 +22,8 @@
:navigationList="navigationList" :navigationList="navigationList"
@add="openEdit" @add="openEdit"
@remove="removeBatch" @remove="removeBatch"
@batchUp="batchUpShelf"
@batchDown="batchDownShelf"
@batchMove="openMove" @batchMove="openMove"
@import="openImport" @import="openImport"
@imageImport="openImageImport" @imageImport="openImageImport"
@@ -53,6 +56,18 @@
/></span> /></span>
</a-space> </a-space>
</template> </template>
<template v-if="column.key === 'isTop'">
<a-space @click.stop="onTop(record)">
<span
v-if="record.isTop === 1"
class="ele-text-success cursor-pointer"
><CheckOutlined
/></span>
<span v-else class="ele-text-placeholder cursor-pointer"
><CloseOutlined
/></span>
</a-space>
</template>
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">已上架</a-tag> <a-tag v-if="record.status === 0" color="green">已上架</a-tag>
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag> <a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
@@ -63,6 +78,19 @@
<a-space> <a-space>
<a @click.stop="openEdit(record)">修改</a> <a @click.stop="openEdit(record)">修改</a>
<a-divider type="vertical" /> <a-divider type="vertical" />
<a
v-if="record.status === 0"
class="ele-text-warning"
@click.stop="toggleShelf(record, 1)"
>下架</a
>
<a
v-else
class="ele-text-success"
@click.stop="toggleShelf(record, 0)"
>上架</a
>
<a-divider type="vertical" />
<a-popconfirm <a-popconfirm
title="确定要删除此记录吗?" title="确定要删除此记录吗?"
@confirm.stop="remove(record)" @confirm.stop="remove(record)"
@@ -242,6 +270,13 @@
key: 'recommend', key: 'recommend',
align: 'center' align: 'center'
}, },
{
title: '置顶',
dataIndex: 'isTop',
key: 'isTop',
align: 'center',
sorter: true
},
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
@@ -259,7 +294,8 @@
dataIndex: 'sortNumber', dataIndex: 'sortNumber',
key: 'sortNumber', key: 'sortNumber',
align: 'center', align: 'center',
width: 120 width: 120,
sorter: true
}, },
{ {
title: '创建时间', title: '创建时间',
@@ -270,15 +306,15 @@
ellipsis: true, ellipsis: true,
width: 180, width: 180,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss') customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right',
align: 'center',
hideInSetting: true
} }
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]); ]);
/* 搜索 */ /* 搜索 */
@@ -506,6 +542,76 @@
}); });
}; };
/* 单个商品置顶/取消置顶isTop: 1 置顶 0 不置顶) */
const onTop = (row: ShopGoods) => {
const hide = message.loading('请求中..', 0);
updateShopGoods({
...row,
isTop: row.isTop == 1 ? 0 : 1
})
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 单个商品上/下架切换status: 0 上架 1 下架) */
const toggleShelf = (row: ShopGoods, status: number) => {
const hide = message.loading('请求中..', 0);
updateShopGoods({ ...row, status })
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量上架 */
const batchUpShelf = () => {
batchSetShelf(0, '上架');
};
/* 批量下架 */
const batchDownShelf = () => {
batchSetShelf(1, '下架');
};
/* 批量设置上下架 */
const batchSetShelf = (status: number, label: string) => {
if (!selection.value.length) {
message.error('请至少选择一条商品');
return;
}
Modal.confirm({
title: '提示',
content: `确定要批量${label}选中的 ${selection.value.length} 个商品吗?`,
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
Promise.all(selection.value.map((d) => updateShopGoods({ ...d, status })))
.then(() => {
hide();
message.success(`已批量${label} ${selection.value.length} 个商品`);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */ /* 自定义行属性 */
const customRow = (record: ShopGoods) => { const customRow = (record: ShopGoods) => {
return { return {

View File

@@ -24,7 +24,7 @@
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'"> <template v-if="column.key === 'title'">
<a-space :style="{ paddingLeft: ((depthMap.get(record.categoryId || 0) || 0) * 20) + 'px' }"> <a-space :style="{ paddingLeft: ((depthMap.get(record.categoryId || 0) || 0) * 20) + 'px' }">
<img v-if="record.image" :src="record.image" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" /> <img v-if="record.image" :src="getCompressedImageUrl(record.image, { width: 48, quality: 90 })" style="width: 24px; height: 24px; object-fit: cover; border-radius: 4px;" />
<span class="font-bold">{{ record.title }}</span> <span class="font-bold">{{ record.title }}</span>
</a-space> </a-space>
</template> </template>
@@ -36,7 +36,7 @@
<template v-if="column.key === 'adImage'"> <template v-if="column.key === 'adImage'">
<img <img
v-if="record.adImage" v-if="record.adImage"
:src="record.adImage" :src="getCompressedImageUrl(record.adImage, { width: 144, quality: 85 })"
style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;" style="width: 72px; height: 40px; object-fit: cover; border-radius: 4px;"
/> />
<span v-else>-</span> <span v-else>-</span>
@@ -91,6 +91,7 @@
} from 'ele-admin-pro/es/ele-pro-table/types'; } from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue'; import Search from './components/search.vue';
import ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue'; import ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue';
import { getCompressedImageUrl } from '@/utils/image';
import { import {
listShopGoodsCategory, listShopGoodsCategory,
removeShopGoodsCategory, removeShopGoodsCategory,