feat(shopGoods): 新增商品置顶功能及批量上下架操作
- 在商品模型中新增 isTop 字段支持置顶状态(1置顶,0不置顶) - 商品列表新增置顶列,支持列表内联切换置顶状态 - 启用排序号列排序功能,置顶列参与前端排序 - 新增单个商品上架/下架切换接口及批量上下架操作 - 优化商品列表工具栏和选中操作栏,加入批量上架、下架按钮 - 商品分类及图片列表调用 OSS 图片压缩接口处理展示图片 - 新增删除商品导入模板及图片导入模板下载链接与使用说明 - 优化图片压缩工具,新增去除 URL 中 x-oss-process 参数功能,确保调用参数生效
This commit is contained in:
15
.workbuddy/memory/2026-07-24.md
Normal file
15
.workbuddy/memory/2026-07-24.md
Normal 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。
|
||||
@@ -21,3 +21,12 @@
|
||||
- `2` = 商家送货
|
||||
|
||||
来源:管理后台 `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
35
add-is-top-shop-goods.sql
Normal 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;
|
||||
37
offline-noimage-goods-10332.sql
Normal file
37
offline-noimage-goods-10332.sql
Normal 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 = '[]')
|
||||
-- ------------------------------------------------------------
|
||||
@@ -87,6 +87,8 @@ export interface ShopGoods {
|
||||
gainIntegral?: number;
|
||||
// 推荐
|
||||
recommend?: number;
|
||||
// 置顶(1置顶 0不置顶)
|
||||
isTop?: number;
|
||||
// 商户ID
|
||||
merchantId?: number;
|
||||
// 商户名称
|
||||
|
||||
@@ -41,6 +41,26 @@ export function ensureFullUrl(url: string): string {
|
||||
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
|
||||
*
|
||||
@@ -49,8 +69,11 @@ export function ensureFullUrl(url: string): string {
|
||||
*
|
||||
* 注意:预览原图时不需要调用此函数,直接用原始 URL 即可。
|
||||
*
|
||||
* 优先级:以调用方指定的 width/quality 为准。若 URL 已自带 x-oss-process
|
||||
* (接口已返回压缩参数),会先剥离再按本函数参数重新拼接,确保调用方参数生效。
|
||||
*
|
||||
* @param url 原始图片 URL
|
||||
* @param options 压缩选项
|
||||
* @param options 压缩选项(width/quality 不传则使用默认值)
|
||||
* @returns 处理后的图片 URL
|
||||
*/
|
||||
export function getCompressedImageUrl(
|
||||
@@ -66,11 +89,12 @@ export function getCompressedImageUrl(
|
||||
// 未启用压缩,原样返回
|
||||
if (!enabled) return fullUrl;
|
||||
|
||||
// 已包含 OSS 处理参数,避免重复拼接
|
||||
if (fullUrl.includes('x-oss-process')) return fullUrl;
|
||||
// 优先使用调用方指定的 width/quality:
|
||||
// 先剥离 URL 自带的 x-oss-process,再按本函数参数重新拼接,确保以调用方为准。
|
||||
const baseUrl = removeOssProcess(fullUrl);
|
||||
|
||||
// 已有其他 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}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<div class="goods-toolbar">
|
||||
<!-- 常驻工具栏 -->
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
@@ -7,50 +9,35 @@
|
||||
</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">
|
||||
|
||||
<!-- 导入(合并为下拉菜单) -->
|
||||
<a-dropdown>
|
||||
<a-button type="dashed" class="ele-btn-icon">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
</template>
|
||||
<span>导入</span>
|
||||
<DownOutlined class="ele-btn-icon-arrow" />
|
||||
</a-button>
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="openImageImport">
|
||||
<template #icon>
|
||||
<UploadOutlined />
|
||||
<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>
|
||||
<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-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 }})
|
||||
@@ -62,6 +49,7 @@
|
||||
>已售罄({{ goodsCount?.totalNum3 }})
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
:tree-data="navigationList"
|
||||
@@ -84,6 +72,41 @@
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -20,16 +20,53 @@
|
||||
</a-upload-dragger>
|
||||
</a-spin>
|
||||
<div class="ele-text-center">
|
||||
<span>只能上传 xls、xlsx 文件</span>
|
||||
<span>只能上传 xls、xlsx 文件,</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.jpg、2.png、3.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 (
|
||||
![
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
size="small"
|
||||
v-model:selection="selection"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
@@ -21,6 +22,8 @@
|
||||
:navigationList="navigationList"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchUp="batchUpShelf"
|
||||
@batchDown="batchDownShelf"
|
||||
@batchMove="openMove"
|
||||
@import="openImport"
|
||||
@imageImport="openImageImport"
|
||||
@@ -53,6 +56,18 @@
|
||||
/></span>
|
||||
</a-space>
|
||||
</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'">
|
||||
<a-tag v-if="record.status === 0" color="green">已上架</a-tag>
|
||||
<a-tag v-if="record.status === 1" color="orange">待上架</a-tag>
|
||||
@@ -63,6 +78,19 @@
|
||||
<a-space>
|
||||
<a @click.stop="openEdit(record)">修改</a>
|
||||
<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
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm.stop="remove(record)"
|
||||
@@ -242,6 +270,13 @@
|
||||
key: 'recommend',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '置顶',
|
||||
dataIndex: 'isTop',
|
||||
key: 'isTop',
|
||||
align: 'center',
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -259,7 +294,8 @@
|
||||
dataIndex: 'sortNumber',
|
||||
key: 'sortNumber',
|
||||
align: 'center',
|
||||
width: 120
|
||||
width: 120,
|
||||
sorter: true
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
@@ -270,15 +306,15 @@
|
||||
ellipsis: true,
|
||||
width: 180,
|
||||
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) => {
|
||||
return {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'title'">
|
||||
<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>
|
||||
</a-space>
|
||||
</template>
|
||||
@@ -36,7 +36,7 @@
|
||||
<template v-if="column.key === 'adImage'">
|
||||
<img
|
||||
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;"
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
@@ -91,6 +91,7 @@
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ShopGoodsCategoryEdit from './components/shopGoodsCategoryEdit.vue';
|
||||
import { getCompressedImageUrl } from '@/utils/image';
|
||||
import {
|
||||
listShopGoodsCategory,
|
||||
removeShopGoodsCategory,
|
||||
|
||||
Reference in New Issue
Block a user