diff --git a/.workbuddy/memory/2026-07-24.md b/.workbuddy/memory/2026-07-24.md
new file mode 100644
index 0000000..8cf1821
--- /dev/null
+++ b/.workbuddy/memory/2026-07-24.md
@@ -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。
diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md
index 601be4b..4bc18a3 100644
--- a/.workbuddy/memory/MEMORY.md
+++ b/.workbuddy/memory/MEMORY.md
@@ -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=审核不通过。
+
diff --git a/add-is-top-shop-goods.sql b/add-is-top-shop-goods.sql
new file mode 100644
index 0000000..9e5c56a
--- /dev/null
+++ b/add-is-top-shop-goods.sql
@@ -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;
diff --git a/offline-noimage-goods-10332.sql b/offline-noimage-goods-10332.sql
new file mode 100644
index 0000000..50fc168
--- /dev/null
+++ b/offline-noimage-goods-10332.sql
@@ -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 = '[]')
+-- ------------------------------------------------------------
diff --git a/src/api/shop/shopGoods/model/index.ts b/src/api/shop/shopGoods/model/index.ts
index 3756be8..c9fd09d 100644
--- a/src/api/shop/shopGoods/model/index.ts
+++ b/src/api/shop/shopGoods/model/index.ts
@@ -87,6 +87,8 @@ export interface ShopGoods {
gainIntegral?: number;
// 推荐
recommend?: number;
+ // 置顶(1置顶 0不置顶)
+ isTop?: number;
// 商户ID
merchantId?: number;
// 商户名称
diff --git a/src/utils/image.ts b/src/utils/image.ts
index 2872574..70f890f 100644
--- a/src/utils/image.ts
+++ b/src/utils/image.ts
@@ -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}`;
}
diff --git a/src/views/shop/shopGoods/components/search.vue b/src/views/shop/shopGoods/components/search.vue
index 42459a9..0b338b6 100644
--- a/src/views/shop/shopGoods/components/search.vue
+++ b/src/views/shop/shopGoods/components/search.vue
@@ -1,89 +1,112 @@
-