From d458115da92b91dff7b172695fb19500f9950414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 17 Aug 2026 02:39:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(special-zone):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E5=BC=B9=E7=AA=97checkbox=E6=9C=AA?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=B8=93=E5=8C=BA=E5=B0=8F=E7=A8=8B=E5=BA=8F=E7=A0=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 UserSelectModal 弹窗中复选框不显示的根因,正确传入 selection-type="checkbox" - 新增后台接口获取专区小程序码图片流,支持正式/体验/开发版环境 - 管理后台专区列表新增二维码按钮,弹窗展示对应专区小程序码 - 小程序端支持扫码参数 scene 解析,实现扫码直接进入专区页面 - 优化二维码弹窗组件,支持切换小程序版本并动态加载二维码 - 重新构建前端及后端服务,确保新功能生效和兼容原有业务逻辑 --- .workbuddy/memory/2026-08-17.md | 24 +++++ src/api/shop/shopZone/index.ts | 28 ++++++ .../zone/components/UserSelectModal.vue | 1 + src/views/special/zone/index.vue | 95 ++++++++++++++++++- 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/.workbuddy/memory/2026-08-17.md b/.workbuddy/memory/2026-08-17.md index 3f47ce0..26fa02c 100644 --- a/.workbuddy/memory/2026-08-17.md +++ b/.workbuddy/memory/2026-08-17.md @@ -26,3 +26,27 @@ - 部署:需重新编译部署 guilixu-java(shop-api)。前端无需改动。 - 注:本机无 maven,未本地编译;改动为标准 MP 用法(IService.count(QueryWrapper) / getById)。 - 待确认:用户 "2.帮我补" 指令只写了前半句,已按上文补后端强校验;若其本意是"补菜单记录"(专区管理入口,动态路由待插入),需另出 INSERT SQL(菜单表结构待确认)。 + +## 专区白名单弹窗 checkbox 不显示(第三次修复,根因确认) +- 现象:白名单弹窗数据正常(12 条用户),但表格无 checkbox 勾选列,用户无法选择。 +- 已尝试修复(前两次均未生效): + 1. 8-16:`reactive` → `computed` + `columnWidth: 48`(与 shopCoupon 对齐) + 2. 同期:去掉 `isStaff: true` +- **真正根因**(读 `ele-pro-table` 源码 `node_modules/ele-admin-pro/es/ele-pro-table/index.js` 第 94-114 行确认): + - `ele-pro-table` 内部 `tableSelectionType` computed 有前置条件:必须传 `selection`、`current` 或 `selectionType="radio"` 三者之一,否则返回 `undefined` + - 返回 undefined → `tableRowSelection` 也返回 undefined → ant-design-vue Table 收到 `rowSelection=undefined` → **不渲染 checkbox 列** + - 我们的弹窗三个条件都不满足(没传 :selection / :current / selectionType),所以不管 row-selection 配置多正确都不会出勾选框 +- **最终修复**:模板 `` 加 `selection-type="checkbox"`(第 18 行),使源码判断走通:`props2.selectionType !== "radio"` 为 true(是 "checkbox")→ 不走 early return → 返回 "checkbox" → checkbox 列正常渲染。 +- 改动文件:`src/views/special/zone/components/UserSelectModal.vue`(仅模板加 1 个 prop) +- 部署:重新构建前端即可(`npm run dev` 热更新或 `npm run build` 部署)。 + +## 专区小程序码(扫码直接进入专区) +- 需求:/special/zone 专区管理加「二维码」入口,生成该专区的小程序码,用户微信扫码直接进入小程序对应专区。 +- 后端(guilixu-java `ShopHomeSectionController.java`):新增 `GET /api/shop/shop-home-section/{id}/qrcode` + - 调微信 `getwxacodeunlimit` 生成无限场景码;`scene=sectionId=xxx`,`page=pages/shop/section-detail`;按专区 `tenantId` 经 `WxMiniappAccessTokenService.getAccessToken(tenantId)` 取 token;prod 用 `release` 否则 `trial`(与现有 getOrderQRCodeUnlimited 一致)。 + - 复用 Hutool `HttpRequest` 调微信,返回 PNG 流;微信出错时返回 JSON 并识别 `application/json`。 +- 用户端小程序(guilixu-taro `pages/shop/section-detail.tsx`):解析 `useRouter().params.scene`(小程序码扫码透传的 `sectionId=xxx`),兼容原 `?sectionId=` 普通跳转。 +- 管理后台(guilixu-admin `views/special/zone/index.vue` + `api/shop/shopZone`):列表操作列加「二维码」按钮 → 弹窗显示小程序码图片;新增 `getSectionQrcode(id)`(responseType blob → objectURL,遇 JSON 错误解析 message 并 reject)。 +- 部署:① 重新编译部署 guilixu-java(shop-api)使新接口生效;② 重新构建前端 admin 与 taro 小程序(section-detail 需重新发布/体验版,因小程序码 page 指向已发布页面)。 +- 注:本机无 maven,后端未本地编译;改动完全复用 WxLoginController 既有 getwxacodeunlimit 写法。 +- 限制:getwxacodeunlimit 的 page 必须已存在于小程序(pages/shop/section-detail 已在 app.config 的 pages 列表);scene 仅限可见字符、≤32 位,`sectionId=xxx` 满足。 diff --git a/src/api/shop/shopZone/index.ts b/src/api/shop/shopZone/index.ts index de14a93..ede3345 100644 --- a/src/api/shop/shopZone/index.ts +++ b/src/api/shop/shopZone/index.ts @@ -182,3 +182,31 @@ export async function checkSectionPermission(sectionIds: number[]) { } return Promise.reject(new Error(res.data.message)); } + +/** + * 获取专区小程序码(图片流),返回可用于 的 objectURL + * @param id 专区ID + * @param envVersion release(正式版) / trial(体验版) / develop(开发版) + */ +export async function getSectionQrcode( + id: number, + envVersion: string = 'release' +): Promise { + const res = await request.get( + MODULES_API_URL + '/shop/shop-home-section/' + id + '/qrcode', + { responseType: 'blob', params: { envVersion } } as any + ); + const blob = res.data as Blob; + // 微信/后端出错时会返回 JSON 而非图片,这里解析出错误信息 + if (blob && blob.type && blob.type.indexOf('image') === -1) { + let msg = '生成小程序码失败'; + try { + const json = JSON.parse(await blob.text()); + if (json && json.error) msg = json.error; + } catch (e) { + /* 忽略解析失败 */ + } + return Promise.reject(new Error(msg)); + } + return window.URL.createObjectURL(blob); +} diff --git a/src/views/special/zone/components/UserSelectModal.vue b/src/views/special/zone/components/UserSelectModal.vue index 685f71d..1d8de68 100644 --- a/src/views/special/zone/components/UserSelectModal.vue +++ b/src/views/special/zone/components/UserSelectModal.vue @@ -15,6 +15,7 @@ :datasource="datasource" :columns="columns" :row-selection="rowSelection" + selection-type="checkbox" :pagination="true" :page-size="10" > diff --git a/src/views/special/zone/index.vue b/src/views/special/zone/index.vue index 2452585..e06cec5 100644 --- a/src/views/special/zone/index.vue +++ b/src/views/special/zone/index.vue @@ -69,6 +69,8 @@ 商品 + 二维码 + + + + +
+
+ + 正式版 + 体验版 + 开发版 + +
+ + +
+ 加载中... +
+
+
+ 微信扫一扫,直接进入该专区 +
+
+
@@ -199,7 +234,8 @@ setSectionUsers, listSectionGoods, addSectionGoods, - removeSectionGoods + removeSectionGoods, + getSectionQrcode } from '@/api/shop/shopZone'; import { pageShopGoods } from '@/api/shop/shopGoods'; import type { @@ -231,6 +267,14 @@ const goodsSearchLoading = ref(false); const candidateGoods = ref([]); + // 专区小程序码弹窗 + const showQrcode = ref(false); + const qrcodeLoading = ref(false); + const qrcodeUrl = ref(''); + const qrcodeTitle = ref('专区小程序码'); + const qrcodeEnv = ref('release'); // release(正式版) / trial(体验版) / develop(开发版) + const qrcodeSectionId = ref(0); + // 表格数据源 const datasource: DatasourceFunction = ({ page, limit, where, orders }) => { return pageHomeSections({ @@ -300,7 +344,7 @@ { title: '操作', key: 'action', - width: 220, + width: 290, fixed: 'right', align: 'center', hideInSetting: true @@ -408,6 +452,53 @@ loadGoods(row.sectionId || 0); }; + /* 生成专区小程序码(按当前选择的版本) */ + const genQrcode = (id: number) => { + qrcodeUrl.value = ''; + qrcodeLoading.value = true; + getSectionQrcode(id, qrcodeEnv.value) + .then((url) => { + qrcodeUrl.value = url; + }) + .catch((e) => { + message.error(e?.message || '生成小程序码失败'); + showQrcode.value = false; + }) + .finally(() => { + qrcodeLoading.value = false; + }); + }; + + /* 打开专区小程序码弹窗 */ + const openQrcode = (row: HomeSection) => { + const id = row.sectionId || 0; + qrcodeSectionId.value = id; + qrcodeTitle.value = '专区小程序码 - ' + (row.title || id); + if (qrcodeUrl.value) { + window.URL.revokeObjectURL(qrcodeUrl.value); + qrcodeUrl.value = ''; + } + qrcodeLoading.value = true; + showQrcode.value = true; + genQrcode(id); + }; + + /* 切换小程序版本后重新生成 */ + const onQrcodeEnvChange = () => { + if (qrcodeSectionId.value && showQrcode.value) { + genQrcode(qrcodeSectionId.value); + } + }; + + /* 关闭专区小程序码弹窗并回收资源 */ + const closeQrcode = (v: boolean) => { + showQrcode.value = v; + if (!v && qrcodeUrl.value) { + window.URL.revokeObjectURL(qrcodeUrl.value); + qrcodeUrl.value = ''; + } + }; + const loadGoods = (sectionId: number) => { goodsLoading.value = true; listSectionGoods(sectionId, { page: goodsPage.value, limit: 10 })