From b9122fd6c85e875227a26be0a7e8193118cf9d3e Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 22 Oct 2026 15:41:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=81=A2=E5=A4=8D=E5=95=86=E5=93=81=E5=92=8C?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/application/extra/common.php | 4 + frontend/miniapp/app.json | 6 + frontend/miniapp/custom-tab-bar/data.js | 6 + frontend/miniapp/pages/cart/index.js | 7 + frontend/miniapp/pages/cart/index.wxml | 1 + .../coupon/coupon-activity-goods/index.js | 6 + .../coupon/coupon-activity-goods/index.wxml | 1 + .../goods/details/components/buy-bar/index.js | 33 ++ .../details/components/buy-bar/index.json | 4 + .../details/components/buy-bar/index.wxml | 15 + .../details/components/buy-bar/index.wxss | 31 ++ .../components/goods-specs-popup/index.js | 326 +++++++++++++ .../components/goods-specs-popup/index.json | 10 + .../components/goods-specs-popup/index.wxml | 73 +++ .../components/goods-specs-popup/index.wxss | 290 ++++++++++++ .../components/promotion-popup/index.js | 35 ++ .../components/promotion-popup/index.json | 7 + .../components/promotion-popup/index.wxml | 34 ++ .../components/promotion-popup/index.wxss | 131 ++++++ frontend/miniapp/pages/goods/details/index.js | 428 ++++++++++++++++++ .../miniapp/pages/goods/details/index.json | 18 + .../miniapp/pages/goods/details/index.wxml | 129 ++++++ .../miniapp/pages/goods/details/index.wxss | 426 +++++++++++++++++ frontend/miniapp/pages/goods/list/index.js | 277 ++++++++++++ frontend/miniapp/pages/goods/list/index.json | 16 + frontend/miniapp/pages/goods/list/index.wxml | 40 ++ frontend/miniapp/pages/goods/list/index.wxss | 91 ++++ frontend/miniapp/pages/goods/result/index.js | 8 + .../miniapp/pages/goods/result/index.wxml | 2 +- .../miniapp/pages/message/detail/index.js | 1 + .../pages/order/after-service-detail/index.js | 6 + .../order/after-service-detail/index.wxml | 2 + .../miniapp/pages/order/order-detail/index.js | 6 + .../pages/order/order-detail/index.wxml | 2 +- .../miniapp/pages/promotion-detail/index.js | 6 + .../miniapp/pages/promotion-detail/index.wxml | 1 + frontend/miniapp/project.config.json | 12 + 37 files changed, 2489 insertions(+), 2 deletions(-) create mode 100644 frontend/miniapp/pages/goods/details/components/buy-bar/index.js create mode 100644 frontend/miniapp/pages/goods/details/components/buy-bar/index.json create mode 100644 frontend/miniapp/pages/goods/details/components/buy-bar/index.wxml create mode 100644 frontend/miniapp/pages/goods/details/components/buy-bar/index.wxss create mode 100644 frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.js create mode 100644 frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.json create mode 100644 frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxml create mode 100644 frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxss create mode 100644 frontend/miniapp/pages/goods/details/components/promotion-popup/index.js create mode 100644 frontend/miniapp/pages/goods/details/components/promotion-popup/index.json create mode 100644 frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxml create mode 100644 frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxss create mode 100644 frontend/miniapp/pages/goods/details/index.js create mode 100644 frontend/miniapp/pages/goods/details/index.json create mode 100644 frontend/miniapp/pages/goods/details/index.wxml create mode 100644 frontend/miniapp/pages/goods/details/index.wxss create mode 100644 frontend/miniapp/pages/goods/list/index.js create mode 100644 frontend/miniapp/pages/goods/list/index.json create mode 100644 frontend/miniapp/pages/goods/list/index.wxml create mode 100644 frontend/miniapp/pages/goods/list/index.wxss diff --git a/backend/application/extra/common.php b/backend/application/extra/common.php index 021e44b..3254b66 100644 --- a/backend/application/extra/common.php +++ b/backend/application/extra/common.php @@ -39,4 +39,8 @@ return [ // 免确认收款授权结果回调地址(公网可访问、HTTPS、不带参数) 'transfer_authorization_notify_url' => Env::get('app.url') . '/api/account/transferAuthorizationNotify', ], + 'virtual_pay' => [ + 'android_pay_fees' => 0.01, + 'ios_pay_fees' => 0.12, + ], ]; \ No newline at end of file diff --git a/frontend/miniapp/app.json b/frontend/miniapp/app.json index 2c9a5c6..0ef1ca0 100644 --- a/frontend/miniapp/app.json +++ b/frontend/miniapp/app.json @@ -5,6 +5,8 @@ "pages/usercenter/person-info/index", "pages/usercenter/address/list/index", "pages/usercenter/address/edit/index", + "pages/goods/list/index", + "pages/goods/details/index", "pages/goods/category/index", "pages/goods/search/index", "pages/goods/result/index", @@ -49,6 +51,10 @@ "pagePath": "pages/home/home", "text": "首页" }, + { + "pagePath": "pages/goods/list/index", + "text": "软件" + }, { "pagePath": "pages/message/list/index", "text": "消息" diff --git a/frontend/miniapp/custom-tab-bar/data.js b/frontend/miniapp/custom-tab-bar/data.js index b45c7df..3e844ff 100644 --- a/frontend/miniapp/custom-tab-bar/data.js +++ b/frontend/miniapp/custom-tab-bar/data.js @@ -4,6 +4,12 @@ export default [{ text: '首页', url: 'pages/home/home', }, +{ + key: 'goods', + icon: 'sort', + text: '软件', + url: 'pages/goods/list/index', +}, { key: 'message', icon: 'comment', diff --git a/frontend/miniapp/pages/cart/index.js b/frontend/miniapp/pages/cart/index.js index 153b121..e710106 100644 --- a/frontend/miniapp/pages/cart/index.js +++ b/frontend/miniapp/pages/cart/index.js @@ -236,6 +236,13 @@ Page({ }); }, + goGoodsDetail(e) { + const { spuId, storeId } = e.detail.goods; + wx.navigateTo({ + url: `/pages/goods/details/index?spuId=${spuId}&storeId=${storeId}`, + }); + }, + clearInvalidGoods() { // 实际场景时应该调用接口清空失效商品 this.clearInvalidGoodsService().then(() => this.refreshData()); diff --git a/frontend/miniapp/pages/cart/index.wxml b/frontend/miniapp/pages/cart/index.wxml index 7a4586a..8828092 100644 --- a/frontend/miniapp/pages/cart/index.wxml +++ b/frontend/miniapp/pages/cart/index.wxml @@ -7,6 +7,7 @@ bindselectstore="onStoreSelect" bindchangequantity="onQuantityChange" bindgocollect="goCollect" + bindgoodsclick="goGoodsDetail" bindclearinvalidgoods="clearInvalidGoods" binddelete="onGoodsDelete" /> diff --git a/frontend/miniapp/pages/coupon/coupon-activity-goods/index.js b/frontend/miniapp/pages/coupon/coupon-activity-goods/index.js index 30dafd6..bf08d36 100644 --- a/frontend/miniapp/pages/coupon/coupon-activity-goods/index.js +++ b/frontend/miniapp/pages/coupon/coupon-activity-goods/index.js @@ -62,6 +62,12 @@ Page({ }); }, + goodClickHandle(e) { + const { index } = e.detail; + const { spuId } = this.data.goods[index]; + wx.navigateTo({ url: `/pages/goods/details/index?spuId=${spuId}` }); + }, + cartClickHandle() { Toast({ context: this, diff --git a/frontend/miniapp/pages/coupon/coupon-activity-goods/index.wxml b/frontend/miniapp/pages/coupon/coupon-activity-goods/index.wxml index edeb18d..1c55b7f 100644 --- a/frontend/miniapp/pages/coupon/coupon-activity-goods/index.wxml +++ b/frontend/miniapp/pages/coupon/coupon-activity-goods/index.wxml @@ -11,6 +11,7 @@ diff --git a/frontend/miniapp/pages/goods/details/components/buy-bar/index.js b/frontend/miniapp/pages/goods/details/components/buy-bar/index.js new file mode 100644 index 0000000..8135c94 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/buy-bar/index.js @@ -0,0 +1,33 @@ +Component({ + externalClasses: ['wr-class'], + + options: { multipleSlots: true }, + + properties: { + soldout: { + // 商品是否下架 + type: Boolean, + value: false, + }, + isStock: { + type: Boolean, + value: true, + }, // 是否有库存 + isSlotButton: { + type: Boolean, + value: false, + }, // 是否开启按钮插槽 + buttonType: { + type: Number, + value: 0, + }, + }, + + methods: { + toBuyNow(e) { + const { isStock, soldout } = this.properties; + if (soldout || !isStock) return; + this.triggerEvent('toBuyNow', e); + }, + }, +}); diff --git a/frontend/miniapp/pages/goods/details/components/buy-bar/index.json b/frontend/miniapp/pages/goods/details/components/buy-bar/index.json new file mode 100644 index 0000000..59a1db6 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/buy-bar/index.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/frontend/miniapp/pages/goods/details/components/buy-bar/index.wxml b/frontend/miniapp/pages/goods/details/components/buy-bar/index.wxml new file mode 100644 index 0000000..e539d31 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/buy-bar/index.wxml @@ -0,0 +1,15 @@ + + + + {{soldout ? '商品已下架' : (!isStock ? '商品已售罄' : '立即购买')}} + + + + + + diff --git a/frontend/miniapp/pages/goods/details/components/buy-bar/index.wxss b/frontend/miniapp/pages/goods/details/components/buy-bar/index.wxss new file mode 100644 index 0000000..22ffd90 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/buy-bar/index.wxss @@ -0,0 +1,31 @@ +.footer-cont { + display: flex; + align-items: center; + padding: 16rpx 24rpx; + background-color: var(--bg-card); +} + +.bar-buy { + display: flex; + flex: 1; + align-items: center; + justify-content: center; + height: 88rpx; + font-size: 30rpx; + font-weight: 600; + color: var(--text-inverse); + background: var(--brand-gradient); + border-radius: var(--radius-round); + box-shadow: var(--shadow-brand); + transition: opacity 0.16s ease; +} + +.bar-buy--hover { + opacity: 0.88; +} + +.bar-buy-disabled { + color: var(--text-disabled) !important; + background: var(--bg-grey) !important; + box-shadow: none; +} diff --git a/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.js b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.js new file mode 100644 index 0000000..25fffde --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.js @@ -0,0 +1,326 @@ +/* eslint-disable no-param-reassign */ +/* eslint-disable no-nested-ternary */ +import Toast from 'tdesign-miniprogram/toast/index'; + +Component({ + options: { + multipleSlots: true, + addGlobalClass: true, + }, + + properties: { + src: { + type: String, + }, + title: String, + show: { + type: Boolean, + value: false, + }, + limitBuyInfo: { + type: String, + value: '', + }, + isStock: { + type: Boolean, + value: true, + }, + limitMaxCount: { + type: Number, + value: 999, + }, + limitMinCount: { + type: Number, + value: 1, + }, + skuList: { + type: Array, + value: [], + observer(skuList) { + if (skuList && skuList.length > 0) { + if (this.initStatus) { + this.initData(); + } + } + }, + }, + specList: { + type: Array, + value: [], + observer(specList) { + if (specList && specList.length > 0) { + this.initData(); + } + }, + }, + outOperateStatus: { + type: Boolean, + value: false, + }, + hasAuth: { + type: Boolean, + value: false, + }, + count: { + type: Number, + value: 1, + observer(count) { + this.setData({ + buyNum: count, + }); + }, + }, + }, + + initStatus: false, + selectedSku: {}, + selectSpecObj: {}, + + data: { + buyNum: 1, + isAllSelectedSku: false, + }, + + methods: { + initData() { + const { skuList } = this.properties; + const { specList } = this.properties; + specList.forEach((item) => { + if (item.specValueList.length > 0) { + item.specValueList.forEach((subItem) => { + const obj = this.checkSkuStockQuantity(subItem.specValueId, skuList); + subItem.hasStockObj = obj; + }); + } + }); + const selectedSku = {}; + specList.forEach((item) => { + selectedSku[item.specId] = ''; + }); + this.setData({ + specList, + }); + this.selectSpecObj = {}; + this.selectedSku = {}; + this.initStatus = true; + }, + + checkSkuStockQuantity(specValueId, skuList) { + let hasStock = false; + const array = []; + skuList.forEach((item) => { + (item.specInfo || []).forEach((subItem) => { + if (subItem.specValueId === specValueId && item.quantity > 0) { + const subArray = []; + (item.specInfo || []).forEach((specItem) => { + subArray.push(specItem.specValueId); + }); + array.push(subArray); + hasStock = true; + } + }); + }); + return { + hasStock, + specsArray: array, + }; + }, + + chooseSpecValueId(specValueId, specId) { + const { selectSpecObj } = this; + const { skuList, specList } = this.properties; + if (selectSpecObj[specId]) { + selectSpecObj[specId] = []; + this.selectSpecObj = selectSpecObj; + } else { + selectSpecObj[specId] = []; + } + + const itemAllSpecArray = []; + const itemUnSelectArray = []; + const itemSelectArray = []; + specList.forEach((item) => { + if (item.specId === specId) { + const subSpecValueItem = item.specValueList.find((subItem) => subItem.specValueId === specValueId); + let specSelectStatus = false; + item.specValueList.forEach((n) => { + itemAllSpecArray.push(n.hasStockObj.specsArray); + if (n.isSelected) { + specSelectStatus = true; + } + if (n.hasStockObj.hasStock) { + itemSelectArray.push(n.specValueId); + } else { + itemUnSelectArray.push(n.specValueId); + } + }); + if (specSelectStatus) { + selectSpecObj[specId] = this.flatten(subSpecValueItem?.hasStockObj.specsArray.concat(itemSelectArray)); + } else { + const subSet = function (arr1, arr2) { + const set2 = new Set(arr2); + const subset = []; + arr1.forEach((val) => { + if (!set2.has(val)) { + subset.push(val); + } + }); + return subset; + }; + selectSpecObj[specId] = subSet(this.flatten(itemAllSpecArray), this.flatten(itemUnSelectArray)); + } + } else { + // 未点击规格的逻辑 + const itemSelectArray = []; + let specSelectStatus = false; + item.specValueList.map( + // 找到有库存的规格数组 + (n) => { + itemSelectArray.push(n.hasStockObj.specsArray); + if (n.isSelected) { + specSelectStatus = true; + } + n.hasStockObj.hasStock = true; + return n; + }, + ); + if (specSelectStatus) { + selectSpecObj[item.specId] = this.flatten(itemSelectArray); + } else { + delete selectSpecObj[item.specId]; + } + } + this.selectSpecObj = selectSpecObj; + }); + const combatArray = Object.values(selectSpecObj); + if (combatArray.length > 0) { + const showArray = combatArray.reduce((x, y) => this.getIntersection(x, y)); + const lastResult = Array.from(new Set(showArray)); + specList.forEach((item) => { + item.specValueList.forEach((subItem) => { + if (lastResult.includes(subItem.specValueId)) { + subItem.hasStockObj.hasStock = true; + } else { + subItem.hasStockObj.hasStock = false; + } + }); + }); + } else { + specList.forEach((item) => { + if (item.specValueList.length > 0) { + item.specValueList.forEach((subItem) => { + const obj = this.checkSkuStockQuantity(subItem.specValueId, skuList); + subItem.hasStockObj = obj; + }); + } + }); + } + this.setData({ + specList, + }); + }, + + flatten(input) { + const stack = [...input]; + const res = []; + while (stack.length) { + const next = stack.pop(); + if (Array.isArray(next)) { + stack.push(...next); + } else { + res.push(next); + } + } + return res.reverse(); + }, + + getIntersection(array, nextArray) { + return array.filter((item) => nextArray.includes(item)); + }, + + toChooseItem(e) { + const { isStock } = this.properties; + if (!isStock) return; + const { id } = e.currentTarget.dataset; + const specId = e.currentTarget.dataset.specid; + const hasStock = e.currentTarget.dataset.hasstock; + if (!hasStock) { + Toast({ + context: this, + selector: '#t-toast', + message: '该规格已售罄', + icon: '', + duration: 1000, + }); + return; + } + + let { selectedSku } = this; + const { specList } = this.properties; + selectedSku = + selectedSku[specId] === id ? { ...this.selectedSku, [specId]: '' } : { ...this.selectedSku, [specId]: id }; + specList.forEach((item) => { + item.specValueList.forEach((valuesItem) => { + if (item.specId === specId) { + valuesItem.isSelected = valuesItem.specValueId === selectedSku[specId]; + } + }); + }); + this.chooseSpecValueId(id, specId); + const isAllSelectedSku = this.isAllSelected(specList, selectedSku); + if (!isAllSelectedSku) { + this.setData({ + selectSkuSellsPrice: 0, + selectSkuImg: '', + }); + } + this.setData({ + specList, + isAllSelectedSku, + }); + this.selectedSku = selectedSku; + this.triggerEvent('change', { + specList, + selectedSku, + isAllSelectedSku, + }); + }, + + // 判断是否所有的sku都已经选中 + isAllSelected(skuTree, selectedSku) { + const selected = Object.keys(selectedSku).filter((skuKeyStr) => selectedSku[skuKeyStr] !== ''); + return skuTree.length === selected.length; + }, + + handlePopupHide() { + this.triggerEvent('closeSpecsPopup', { + show: false, + }); + }, + + specsConfirm() { + const { isStock } = this.properties; + if (!isStock) return; + this.triggerEvent('specsConfirm'); + }, + + buyNow() { + const { isAllSelectedSku } = this.data; + const { isStock } = this.properties; + if (!isStock) return; + this.triggerEvent('buyNow', { + isAllSelectedSku, + }); + }, + + // 总处理 + setBuyNum(buyNum) { + this.setData({ + buyNum, + }); + this.triggerEvent('changeNum', { + buyNum, + }); + }, + }, +}); diff --git a/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.json b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.json new file mode 100644 index 0000000..34d167e --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.json @@ -0,0 +1,10 @@ +{ + "component": true, + "usingComponents": { + "t-popup": "tdesign-miniprogram/popup/popup", + "t-icon": "tdesign-miniprogram/icon/icon", + "t-image": "/components/webp-image/index", + "t-stepper": "tdesign-miniprogram/stepper/stepper", + "t-toast": "tdesign-miniprogram/toast/toast" + } +} \ No newline at end of file diff --git a/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxml b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxml new file mode 100644 index 0000000..b24ff77 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxml @@ -0,0 +1,73 @@ + + + + + + + + + {{title}} + + + + + + 选择: + + + {{selectedItem.specValue}} + + + + + + + + + {{item.title}} + + + {{valuesItem.specValue}} + + + + + + + + 购买数量 + + + + + + + 确定 + + + + 立即购买 + + + + + + diff --git a/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxss b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxss new file mode 100644 index 0000000..57a76db --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/goods-specs-popup/index.wxss @@ -0,0 +1,290 @@ +.popup-container { + background-color: #ffffff; + position: relative; + z-index: 100; + border-radius: 16rpx 16rpx 0 0; + padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx); +} + +.popup-container .popup-close { + position: absolute; + right: 30rpx; + top: 30rpx; + z-index: 9; + color: var(--text-tertiary); +} + +.popup-sku-header { + display: flex; + padding: 30rpx 28rpx 0 30rpx; +} + +.popup-sku-header .popup-sku-header__img { + width: 176rpx; + height: 176rpx; + border-radius: var(--radius-md); + background: var(--bg-grey); + margin-right: 24rpx; +} + +.popup-sku-header .popup-sku-header__goods-info { + position: relative; + width: 500rpx; +} + +.popup-sku-header .popup-sku-header__goods-info .popup-sku__goods-name { + display: -webkit-box; + width: 430rpx; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + font-size: 30rpx; + font-weight: 500; + line-height: 42rpx; + color: var(--text-primary); + white-space: normal; + text-overflow: ellipsis; +} + +.popup-sku-header .popup-sku-header__goods-info .popup-sku__selected-spec { + display: flex; + color: var(--text-primary); + font-size: 26rpx; + line-height: 36rpx; +} + +.popup-sku-header + .popup-sku-header__goods-info + .popup-sku__selected-spec + .popup-sku__selected-item { + margin-right: 10rpx; +} + +.popup-sku-body { + margin: 0 30rpx 40rpx; + max-height: 600rpx; + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} + +.popup-sku-body .popup-sku-group-container .popup-sku-row { + padding: 32rpx 0; + border-bottom: 1rpx solid var(--bg-grey); +} + +.popup-sku-body + .popup-sku-group-container + .popup-sku-row + .popup-sku-row__title { + font-size: 26rpx; + color: var(--text-primary); +} + +.popup-sku-body .popup-sku-group-container .popup-sku-row .popup-sku-row__item { + font-size: 24rpx; + color: var(--text-primary); + min-width: 128rpx; + height: 56rpx; + background-color: var(--bg-grey); + border-radius: 8rpx; + border: 2rpx solid var(--bg-grey); + margin: 19rpx 26rpx 0 0; + padding: 0 16rpx; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.popup-sku-body + .popup-sku-group-container + .popup-sku-row + .popup-sku-row__item.popup-sku-row__item--active { + border: 2rpx solid var(--brand); + color: var(--brand); + background: rgba(255, 95, 21, 0.04); +} + +.popup-sku-body + .popup-sku-group-container + .popup-sku-row + .disabled-sku-selected { + background: var(--bg-grey) !important; + color: #cccccc; +} + +.popup-sku-body .popup-sku-stepper-stock .popup-sku-stepper-container { + display: flex; + align-items: center; + justify-content: space-between; + margin: 40rpx 0; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-sku__stepper-title { + display: flex; + font-size: 26rpx; + color: var(--text-primary); +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-sku__stepper-title + .limit-text { + margin-left: 10rpx; + color: var(--text-tertiary); +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper { + display: flex; + flex-flow: row nowrap; + align-items: center; + font-size: 28px; + height: 48rpx; + line-height: 62rpx; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .input-btn, +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .input-num-wrap { + position: relative; + height: 100%; + text-align: center; + background-color: var(--bg-grey); + border-radius: 4rpx; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .input-num-wrap { + color: var(--text-primary); + display: flex; + max-width: 76rpx; + align-items: center; + justify-content: space-between; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .input-num-wrap + .input-num { + height: 100%; + width: auto; + font-weight: 600; + font-size: 30rpx; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .input-btn { + width: 48rpx; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .popup-stepper__minus { + margin-right: 4rpx; + border-radius: 4rpx; + color: #9a979b; + display: flex; + align-items: center; + justify-content: center; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .popup-stepper__plus { + margin-left: 4rpx; + border-radius: 4rpx; + color: #9a979b; + display: flex; + align-items: center; + justify-content: center; +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .popup-stepper__plus::after { + width: 24rpx; + height: 3rpx; + background-color: var(--text-tertiary); +} + +.popup-sku-body + .popup-sku-stepper-stock + .popup-sku-stepper-container + .popup-stepper + .popup-stepper__plus::before { + width: 3rpx; + height: 24rpx; + background-color: var(--text-tertiary); +} + +.popup-sku-actions { + padding: 0 32rpx; +} + +.popup-sku-actions .selected-sku-btn { + width: 100%; +} + +.popup-sku-actions .sku-operate-buyNow { + height: 88rpx; + font-size: 30rpx; + font-weight: 600; + color: var(--text-inverse); + text-align: center; + line-height: 88rpx; + background: var(--brand-gradient); + border-radius: var(--radius-round); + box-shadow: var(--shadow-brand); +} + +.popup-sku-actions .sku-operate-buyNow.disabled { + color: var(--text-disabled); + background: var(--bg-grey); + box-shadow: none; +} + +.popup-container .single-confirm-btn { + margin: 0 32rpx; + height: 88rpx; + font-size: 30rpx; + font-weight: 600; + color: var(--text-inverse); + text-align: center; + line-height: 88rpx; + background: var(--brand-gradient); + border-radius: var(--radius-round); + box-shadow: var(--shadow-brand); +} + +.popup-container .single-confirm-btn.disabled { + color: var(--text-disabled); + background: var(--bg-grey); + box-shadow: none; +} diff --git a/frontend/miniapp/pages/goods/details/components/promotion-popup/index.js b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.js new file mode 100644 index 0000000..9743cc1 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.js @@ -0,0 +1,35 @@ +Component({ + options: { + multipleSlots: true, + }, + + properties: { + list: Array, + title: { + type: String, + value: '促销说明', + }, + show: { + type: Boolean, + }, + }, + + // data: { + // list: [], + // }, + + methods: { + change(e) { + const { index } = e.currentTarget.dataset; + this.triggerEvent('promotionChange', { + index, + }); + }, + + closePromotionPopup() { + this.triggerEvent('closePromotionPopup', { + show: false, + }); + }, + }, +}); diff --git a/frontend/miniapp/pages/goods/details/components/promotion-popup/index.json b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.json new file mode 100644 index 0000000..a9de77d --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.json @@ -0,0 +1,7 @@ +{ + "component": true, + "usingComponents": { + "t-popup": "tdesign-miniprogram/popup/popup", + "t-icon": "tdesign-miniprogram/icon/icon" + } +} \ No newline at end of file diff --git a/frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxml b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxml new file mode 100644 index 0000000..c1cea9d --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxml @@ -0,0 +1,34 @@ + + + + + + + {{title}} + + + + + {{item.tag}} + + {{item.label ? item.label : ''}} + + + + + + + + + diff --git a/frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxss b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxss new file mode 100644 index 0000000..1dae832 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/components/promotion-popup/index.wxss @@ -0,0 +1,131 @@ +.promotion-popup-container { + background-color: #ffffff; + position: relative; + z-index: 100; + border-radius: 16rpx 16rpx 0 0; +} + +.promotion-popup-container .promotion-popup-close { + position: absolute; + right: 30rpx; + top: 30rpx; + z-index: 9; + color: rgba(153, 153, 153, 1); +} + +.promotion-popup-container .promotion-popup-close .market { + font-size: 25rpx; + color: var(--text-tertiary); +} + +.promotion-popup-container .promotion-popup-title { + height: 100rpx; + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.promotion-popup-container .promotion-popup-title { + font-size: 32rpx; + color: #222427; + font-weight: 600; +} + +.promotion-popup-container .promotion-popup-content { + min-height: 400rpx; + max-height: 600rpx; + padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx); + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} + +.promotion-popup-container .promotion-popup-content .promotion-detail-list { + margin: 0 30rpx; +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item:last-child { + margin-bottom: env(safe-area-inset-bottom); + border-bottom: 0; + padding-bottom: calc(28rpx + env(safe-area-inset-bottom)); +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item { + display: flex; + justify-content: space-between; + padding: 10rpx 0 28rpx; + position: relative; + font-size: 24rpx; + color: #222427; +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item + .tag { + box-sizing: border-box; + font-size: 20rpx; + line-height: 32rpx; + padding: 2rpx 12rpx; + background-color: var(--brand-light); + margin-right: 16rpx; + display: inline-flex; + color: var(--brand); + border-radius: 54rpx; + flex-shrink: 0; + position: relative; + top: 2rpx; +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item + .content { + font-size: 28rpx; + color: #222427; + flex: 1; + line-height: 40rpx; + display: flex; +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item + .content + .list-content { + width: 440rpx; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + display: inline-block; +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item + .collect-btn { + font-size: 24rpx; + flex-shrink: 0; + margin-left: 20rpx; + display: flex; + align-items: center; +} + +.promotion-popup-container + .promotion-popup-content + .promotion-detail-list + .list-item + .collect-btn + .linkText { + margin-right: 8rpx; +} diff --git a/frontend/miniapp/pages/goods/details/index.js b/frontend/miniapp/pages/goods/details/index.js new file mode 100644 index 0000000..27b956c --- /dev/null +++ b/frontend/miniapp/pages/goods/details/index.js @@ -0,0 +1,428 @@ +import { fetchGood } from '../../../services/good/fetchGood'; +import { fetchActivityList } from '../../../services/activity/fetchActivityList'; +import { + getGoodsDetailsCommentList, + getGoodsDetailsCommentsCount, +} from '../../../services/good/fetchGoodsDetailsComments'; + +import { cdnBase } from '../../../config/index'; +import { fetchGoodsCaseView } from '../../../services/home/home'; +import { withPromoter, promoterQuery } from '../../../utils/promoter'; + +const imgPrefix = `${cdnBase}/`; + +const recLeftImg = `${imgPrefix}common/rec-left.png`; +const recRightImg = `${imgPrefix}common/rec-right.png`; +const obj2Params = (obj = {}, encode = false) => { + const result = []; + Object.keys(obj).forEach((key) => + result.push(`${key}=${encode ? encodeURIComponent(obj[key]) : obj[key]}`), + ); + + return result.join('&'); +}; + +Page({ + data: { + commentsList: [], + commentsStatistics: { + badCount: 0, + commentCount: 0, + goodCount: 0, + goodRate: 0, + hasImageCount: 0, + middleCount: 0, + }, + isShowPromotionPop: false, + activityList: [], + recLeftImg, + recRightImg, + details: {}, + goodsTabArray: [ + { + name: '商品', + value: '', // 空字符串代表置顶 + }, + { + name: '详情', + value: 'goods-page', + }, + ], + storeLogo: `${imgPrefix}common/store-logo.png`, + storeName: '云mall标准版旗舰店', + isStock: true, + soldout: false, + buttonType: 1, + buyNum: 1, + selectedAttrStr: '', + skuArray: [], + primaryImage: '', + specImg: '', + isSpuSelectPopupShow: false, + isAllSelectedSku: false, + buyType: 0, + outOperateStatus: false, // 是否由底部购买栏唤起弹窗 + operateType: 0, + selectSkuSellsPrice: 0, + maxLinePrice: 0, + minSalePrice: 0, + maxSalePrice: 0, + list: [], + spuId: '', + navigation: { type: 'fraction' }, + current: 0, + autoplay: true, + duration: 500, + interval: 5000, + soldNum: 0, // 已售数量 + caseView: wx.getStorageSync('goodsCaseView') === true, // 案例展示:标题改为详情,不显示购买和规格 + }, + + handlePopupHide() { + this.setData({ + isSpuSelectPopupShow: false, + }); + }, + + showSkuSelectPopup(type) { + this.setData({ + buyType: type || 0, + outOperateStatus: type >= 1, + isSpuSelectPopupShow: true, + }); + }, + + // 点击"已选"行:仅选择规格 + onSelectSkuTap() { + this.showSkuSelectPopup(0); + }, + + // 点击底部"立即购买" + buyItNow() { + this.showSkuSelectPopup(1); + }, + + showCurImg(e) { + const { index } = e.detail; + const { images } = this.data.details; + wx.previewImage({ + current: images[index], + urls: images, // 需要预览的图片http链接列表 + }); + }, + + onPageScroll({ scrollTop }) { + const goodsTab = this.selectComponent('#goodsTab'); + goodsTab && goodsTab.onScroll(scrollTop); + }, + onVideoError(e) { + console.error('视频播放出错:', e.detail.errMsg); + wx.showToast({ + title: '视频加载失败', + icon: 'none' + }); + }, + chooseSpecItem(e) { + const { specList } = this.data.details; + const { selectedSku, isAllSelectedSku } = e.detail; + if (!isAllSelectedSku) { + this.setData({ + selectSkuSellsPrice: 0, + }); + } + this.setData({ + isAllSelectedSku, + }); + this.getSkuItem(specList, selectedSku); + }, + + getSkuItem(specList, selectedSku) { + const { skuArray, primaryImage } = this.data; + const selectedSkuValues = this.getSelectedSkuValues(specList, selectedSku); + let selectedAttrStr = ` 件 `; + selectedSkuValues.forEach((item) => { + selectedAttrStr += `,${item.specValue} `; + }); + // eslint-disable-next-line array-callback-return + const skuItem = skuArray.filter((item) => { + let status = true; + (item.specInfo || []).forEach((subItem) => { + if ( + !selectedSku[subItem.specId] || + selectedSku[subItem.specId] !== subItem.specValueId + ) { + status = false; + } + }); + if (status) return item; + }); + this.selectSpecsName(selectedSkuValues.length > 0 ? selectedAttrStr : ''); + if (skuItem) { + this.setData({ + selectItem: skuItem, + selectSkuSellsPrice: skuItem.price || 0, + }); + } else { + this.setData({ + selectItem: null, + selectSkuSellsPrice: 0, + }); + } + this.setData({ + specImg: skuItem && skuItem.skuImage ? skuItem.skuImage : primaryImage, + }); + }, + + // 获取已选择的sku名称 + getSelectedSkuValues(skuTree, selectedSku) { + const normalizedTree = this.normalizeSkuTree(skuTree); + return Object.keys(selectedSku).reduce((selectedValues, skuKeyStr) => { + const skuValues = normalizedTree[skuKeyStr]; + const skuValueId = selectedSku[skuKeyStr]; + if (skuValueId !== '') { + const skuValue = skuValues.filter((value) => { + return value.specValueId === skuValueId; + })[0]; + skuValue && selectedValues.push(skuValue); + } + return selectedValues; + }, []); + }, + + normalizeSkuTree(skuTree) { + const normalizedTree = {}; + skuTree.forEach((treeItem) => { + normalizedTree[treeItem.specId] = treeItem.specValueList; + }); + return normalizedTree; + }, + + selectSpecsName(selectSpecsName) { + if (selectSpecsName) { + this.setData({ + selectedAttrStr: selectSpecsName, + }); + } else { + this.setData({ + selectedAttrStr: '', + }); + } + }, + + gotoBuy() { + const { buyNum } = this.data; + this.handlePopupHide(); + const query = { + quantity: buyNum, + storeId: '1', + spuId: this.data.spuId, + goodsName: this.data.details.title, + // skuId: type === 1 ? this.data.skuList[0].skuId : this.data.selectItem.skuId, + available: this.data.details.isAvailableSwitch, + price: this.data.details.price, + // specInfo: this.data.details.specList?.map((item) => ({ + // specTitle: item.title, + // specValue: item.specValueList[0].specValue, + // })), + primaryImage: this.data.details.primaryImage, + spuId: this.data.details.spuId, + thumb: this.data.details.primaryImage, + title: this.data.details.title, + }; + let urlQueryStr = obj2Params({ + goodsRequestList: JSON.stringify([query]), + }, true); + urlQueryStr = urlQueryStr ? `?${urlQueryStr}` : ''; + const path = `/pages/order/order-confirm/index${urlQueryStr}`; + wx.navigateTo({ + url: path, + }); + }, + + specsConfirm() { + this.gotoBuy(); + }, + + changeNum(e) { + this.setData({ + buyNum: e.detail.buyNum, + }); + }, + + closePromotionPopup() { + this.setData({ + isShowPromotionPop: false, + }); + }, + + promotionChange(e) { + const { index } = e.detail; + wx.navigateTo({ + url: `/pages/promotion-detail/index?promotion_id=${index}`, + }); + }, + + showPromotionPopup() { + this.setData({ + isShowPromotionPop: true, + }); + }, + + getDetail(spuId) { + Promise.all([fetchGood(spuId), fetchActivityList()]).then((res) => { + let [details, activityList] = res; + console.log(details); + details = details.data; + console.log(details); + activityList = []; + const skuArray = []; + const { + price, + skuList, + primaryImage, + isPutOnSale, + minSalePrice, + maxSalePrice, + maxLinePrice, + soldNum, + } = details; + skuList.forEach((item) => { + skuArray.push({ + skuId: item.skuId, + quantity: item.stockInfo ? item.stockInfo.stockQuantity : 0, + specInfo: item.specInfo, + }); + }); + const promotionArray = []; + activityList.forEach((item) => { + promotionArray.push({ + tag: item.promotionSubCode === 'MYJ' ? '满减' : '满折', + label: '满100元减99.9元', + }); + }); + this.setData({ + details, + activityList, + isStock: details.spuStockQuantity > 0, + maxSalePrice: maxSalePrice ? parseFloat(maxSalePrice) : 0, + maxLinePrice: maxLinePrice ? parseFloat(maxLinePrice) : 0, + minSalePrice: minSalePrice > 0 ? parseFloat(minSalePrice) : price, + list: promotionArray, + skuArray: skuArray, + primaryImage, + soldout: isPutOnSale === 0, + soldNum, + }); + }); + }, + + async getCommentsList() { + try { + const code = 'Success'; + const data = await getGoodsDetailsCommentList(); + const { homePageComments } = data; + if (code.toUpperCase() === 'SUCCESS') { + const nextState = { + commentsList: homePageComments.map((item) => { + return { + goodsSpu: item.spuId, + userName: item.userName || '', + commentScore: item.commentScore, + commentContent: item.commentContent || '用户未填写评价', + userHeadUrl: item.isAnonymity + ? this.anonymityAvatar + : item.userHeadUrl || this.anonymityAvatar, + }; + }), + }; + this.setData(nextState); + } + } catch (error) { + console.error('comments error:', error); + } + }, + + onShareAppMessage() { + // 自定义的返回信息 + const { selectedAttrStr } = this.data; + let shareSubTitle = ''; + if (selectedAttrStr.indexOf('件') > -1) { + const count = selectedAttrStr.indexOf('件'); + shareSubTitle = selectedAttrStr.slice(count + 1, selectedAttrStr.length); + } + const customInfo = { + imageUrl: this.data.details.primaryImage, + title: this.data.details.title + shareSubTitle, + path: withPromoter(`/pages/goods/details/index?spuId=${this.data.spuId}`), + }; + return customInfo; + }, + + onShareTimeline() { + const query = promoterQuery(); + return { + title: this.data.details.title, + imageUrl: this.data.details.primaryImage, + query: `spuId=${this.data.spuId}${query ? `&${query}` : ''}`, + }; + }, + + /** 获取评价统计 */ + async getCommentsStatistics() { + try { + const code = 'Success'; + const data = await getGoodsDetailsCommentsCount(); + if (code.toUpperCase() === 'SUCCESS') { + const { + badCount, + commentCount, + goodCount, + goodRate, + hasImageCount, + middleCount, + } = data; + const nextState = { + commentsStatistics: { + badCount: parseInt(`${badCount}`), + commentCount: parseInt(`${commentCount}`), + goodCount: parseInt(`${goodCount}`), + /** 后端返回百分比后数据但没有限制位数 */ + goodRate: Math.floor(goodRate * 10) / 10, + hasImageCount: parseInt(`${hasImageCount}`), + middleCount: parseInt(`${middleCount}`), + }, + }; + this.setData(nextState); + } + } catch (error) { + console.error('comments statiistics error:', error); + } + }, + + /** 跳转到评价列表 */ + navToCommentsListPage() { + wx.navigateTo({ + url: `/pages/goods/comments/index?spuId=${this.data.spuId}`, + }); + }, + + onLoad(query) { + const { spuId } = query; + this.setData({ + spuId: spuId, + }); + this.applyCaseView(); + this.getDetail(spuId); + this.getCommentsList(spuId); + this.getCommentsStatistics(spuId); + }, + + async applyCaseView() { + if (this.data.caseView) { + wx.setNavigationBarTitle({ title: '详情' }); + } + const caseView = await fetchGoodsCaseView(); + this.setData({ caseView }); + wx.setNavigationBarTitle({ title: caseView ? '详情' : '商品详情' }); + }, +}); diff --git a/frontend/miniapp/pages/goods/details/index.json b/frontend/miniapp/pages/goods/details/index.json new file mode 100644 index 0000000..0051686 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/index.json @@ -0,0 +1,18 @@ +{ + "navigationBarTitleText": "商品详情", + "usingComponents": { + "t-image": "/components/webp-image/index", + "t-tag": "tdesign-miniprogram/tag/tag", + "t-toast": "tdesign-miniprogram/toast/toast", + "t-rate": "tdesign-miniprogram/rate/rate", + "t-swiper": "tdesign-miniprogram/swiper/swiper", + "t-swiper-nav": "tdesign-miniprogram/swiper-nav/swiper-nav", + "t-button": "tdesign-miniprogram/button/button", + "t-icon": "tdesign-miniprogram/icon/icon", + "t-popup": "tdesign-miniprogram/popup/popup", + "price": "/components/price/index", + "buy-bar": "./components/buy-bar/index", + "promotion-popup": "./components/promotion-popup/index", + "goods-specs-popup": "./components/goods-specs-popup/index" + } +} \ No newline at end of file diff --git a/frontend/miniapp/pages/goods/details/index.wxml b/frontend/miniapp/pages/goods/details/index.wxml new file mode 100644 index 0000000..e6f4568 --- /dev/null +++ b/frontend/miniapp/pages/goods/details/index.wxml @@ -0,0 +1,129 @@ + + + + + + + + + + 起 + + + 已售 {{soldNum}} + + + + + + {{item.tag}} + + + + 领劵 + + + + + + {{details.title}} + + + + + + + + + + + + + 3分钟客服响应 · 平台监管售后 + + + {{details.intro}} + + + + 已选 + + + {{selectedAttrStr ? buyNum : ''}}{{selectedAttrStr || '请选择规格'}} + + + + + + + + + 商品评价 + ({{ commentsStatistics.commentCount }}) + + + {{commentsStatistics.goodRate}}% 好评 + + + + + + + + {{commentItem.userName}} + + + + {{commentItem.commentContent}} + + + + + + + + 详情介绍 + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/miniapp/pages/goods/details/index.wxss b/frontend/miniapp/pages/goods/details/index.wxss new file mode 100644 index 0000000..30a845b --- /dev/null +++ b/frontend/miniapp/pages/goods/details/index.wxss @@ -0,0 +1,426 @@ +@import '../../../style/global.wxss'; + +page { + width: 100vw; + background-color: var(--bg-page); +} + +.goods-detail-page { + min-height: 100vh; + padding-bottom: 160rpx; + background-color: var(--bg-page); +} + +.goods-detail-page--case { + padding-bottom: calc(env(safe-area-inset-bottom) + 32rpx); +} + +.goods-head { + --td-swiper-radius: 0; +} + +.goods-gallery { + position: relative; + background-color: var(--bg-card); +} + +/* 商品信息卡片:轻微上浮覆盖轮播,形成层次 */ +.goods-info { + position: relative; + z-index: 2; + margin-top: -24rpx; + padding: 28rpx 30rpx 32rpx; + background-color: var(--bg-card); + border-radius: var(--radius-lg) var(--radius-lg) 0 0; +} + +.goods-info .goods-number { + display: flex; + align-items: center; + justify-content: space-between; +} + +.goods-info .goods-price { + display: flex; + align-items: baseline; +} + +.goods-info .goods-price .class-goods-price { + font-size: 64rpx; + font-weight: 700; + color: var(--brand); + font-family: 'DIN Alternate', -apple-system, sans-serif; +} + +.goods-info .goods-price .class-goods-symbol { + font-size: 36rpx; + color: var(--brand); +} + +.goods-info .goods-price .class-goods-del { + position: relative; + left: 16rpx; + bottom: 2rpx; + font-size: 28rpx; + font-weight: normal; + color: var(--text-tertiary); +} + +.goods-info .goods-price-up { + position: relative; + left: 8rpx; + bottom: 4rpx; + font-size: 26rpx; + color: var(--brand); +} + +.goods-info .goods-number .sold-num { + display: flex; + flex-shrink: 0; + align-items: flex-end; + padding: 4rpx 16rpx; + font-size: 22rpx; + color: var(--text-tertiary); + background: var(--bg-grey); + border-radius: var(--radius-round); +} + +.goods-info .goods-activity { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 20rpx; + padding: 12rpx 20rpx; + background: var(--brand-light); + border-radius: var(--radius-sm); +} + +.goods-info .goods-activity .tags-container { + display: flex; + flex: 1; + overflow: hidden; +} + +.goods-info .goods-activity .tags-container .goods-activity-tag { + margin-right: 16rpx; + padding: 4rpx 12rpx; + font-size: 22rpx; + color: var(--brand); + background: var(--bg-card); + border-radius: var(--radius-sm); +} + +.goods-info .goods-activity .activity-show { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + font-size: 24rpx; + color: var(--brand); +} + +.goods-info .goods-activity .activity-show-text { + line-height: 42rpx; +} + +.goods-info .goods-title { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-top: 24rpx; +} + +.goods-info .goods-title .goods-name { + flex: 1; + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + font-size: 34rpx; + font-weight: 600; + line-height: 48rpx; + color: var(--text-primary); + word-break: break-all; +} + +.goods-info .goods-title .goods-tag { + flex-shrink: 0; + width: 104rpx; + margin-left: 16rpx; +} + +.goods-info .goods-title .goods-tag .shareBtn { + width: 100rpx; + height: 96rpx; + padding-right: 36rpx !important; + border: none; + border-radius: 200rpx 0 0 200rpx; +} + +.goods-info .goods-title .goods-tag .shareBtn::after { + border: none; +} + +.goods-info .goods-title .goods-tag .btn-icon { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 96rpx; + font-size: 20rpx; + color: var(--text-tertiary); +} + +.goods-info .goods-title .goods-tag .btn-icon .share-text { + padding-top: 8rpx; + font-size: 20rpx; + line-height: 24rpx; +} + +.goods-service { + display: flex; + align-items: center; + margin-top: 18rpx; +} + +.goods-service__text { + margin-left: 8rpx; + font-size: 24rpx; + color: var(--text-tertiary); +} + +.goods-info .goods-intro { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + margin-top: 12rpx; + font-size: 26rpx; + line-height: 38rpx; + color: var(--text-tertiary); + word-break: break-all; + white-space: normal; +} + +/* 已选规格 */ +.spu-select { + display: flex; + align-items: center; + margin: 20rpx 24rpx 0; + padding: 28rpx 30rpx; + font-size: 28rpx; + background-color: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); +} + +.spu-select .label { + flex-shrink: 0; + margin-right: 24rpx; + font-weight: normal; + color: var(--text-tertiary); +} + +.spu-select .content { + display: flex; + flex: 1; + align-items: center; + justify-content: space-between; + min-width: 0; +} + +.spu-select .content .content-text { + flex: 1; + overflow: hidden; + color: var(--text-primary); + white-space: nowrap; + text-overflow: ellipsis; +} + +.spu-select .content .tintColor { + color: var(--text-placeholder); +} + +/* 评价 */ +.comments-wrap { + margin: 20rpx 24rpx 0; + padding: 32rpx; + background-color: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); +} + +.comments-wrap .comments-head { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.comments-wrap .comments-head .comments-title-wrap { + display: flex; +} + +.comments-title-label, +.comments-title-count { + font-size: 32rpx; + font-weight: 600; + line-height: 48rpx; + color: var(--text-primary); +} + +.comments-rate-wrap { + display: flex; + align-items: center; + justify-content: center; + font-size: 24rpx; +} + +.comments-rate-wrap .comments-good-rate { + font-size: 26rpx; + font-weight: 400; + font-style: normal; + line-height: 36rpx; + color: var(--text-tertiary); +} + +.comment-item-wrap .comment-item-head { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 32rpx; +} + +.comment-item-wrap .comment-item-head .comment-item-avatar { + width: 64rpx; + height: 64rpx; + border-radius: 64rpx; +} + +.comment-item-wrap .comment-item-head .comment-head-right { + margin-left: 24rpx; +} + +.comment-head-right .comment-username { + font-size: 26rpx; + font-weight: 400; + line-height: 36rpx; + color: var(--text-primary); +} + +.comment-item-wrap .comment-item-content { + margin-top: 20rpx; + font-size: 28rpx; + font-weight: 400; + line-height: 40rpx; + color: var(--text-primary); +} + +/* 详情 */ +.desc-content { + margin: 20rpx 24rpx 0; + padding-bottom: 40rpx; + overflow: hidden; + background-color: var(--bg-card); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-card); +} + +.desc-content__title { + display: flex; + align-items: center; + justify-content: center; + padding: 32rpx 20rpx 16rpx; + font-size: 28rpx; + line-height: 36rpx; + text-align: center; +} + +.desc-content__title .img { + width: 206rpx; + height: 10rpx; +} + +.desc-content__title--text { + margin: 0 32rpx; + font-size: 26rpx; + font-weight: 600; +} + +.desc-content__img { + width: 100%; + height: auto; +} + +.rich-text-content { + margin: 0 20rpx; +} + +.rich-text-content [src] { + display: block; + max-width: 100% !important; + height: auto !important; +} + +.video-container { + padding: 20rpx; +} + +.video-item { + margin-bottom: 10rpx; +} + +.video-player { + width: 100%; + height: 400rpx; + border-radius: var(--radius-md); +} + +/* 底部购买栏 */ +.goods-bottom-operation { + position: fixed; + left: 0; + bottom: 0; + z-index: 100; + width: 100%; + padding-bottom: env(safe-area-inset-bottom); + background-color: var(--bg-card); + box-shadow: 0 -4rpx 20rpx rgba(29, 33, 41, 0.06); +} + +/* 规格弹窗价格 */ +.popup-sku-header .popup-sku-header__goods-info .popup-sku__price { + display: flex; + align-items: baseline; + margin-top: 12rpx; + color: var(--brand); +} + +.popup-sku-header .popup-sku-header__goods-info .popup-sku__price .popup-sku__price-num { + font-size: 56rpx; + font-weight: 700; + color: var(--brand); + font-family: 'DIN Alternate', -apple-system, sans-serif; +} + +.popup-sku-header .popup-sku-header__goods-info .popup-sku__price .popup-sku__price-del { + position: relative; + left: 12rpx; + bottom: 2rpx; + font-size: 28rpx; + font-weight: normal; + color: var(--text-tertiary); +} + +.popup-sku-header .popup-sku-header__goods-info .popup-sku__price .popup-sku__price-symbol { + font-size: 32rpx; + color: var(--brand); +} + +.popup-sku-header .popup-sku-header__goods-info .popup-sku__price .popup-sku__price-max-num { + font-size: 48rpx; +} + +.t-toast__content { + z-index: 12000 !important; +} diff --git a/frontend/miniapp/pages/goods/list/index.js b/frontend/miniapp/pages/goods/list/index.js new file mode 100644 index 0000000..0d2bfc9 --- /dev/null +++ b/frontend/miniapp/pages/goods/list/index.js @@ -0,0 +1,277 @@ +import { fetchGoodsList } from '../../../services/good/fetchGoodsList'; +import { getCategoryList } from '../../../services/good/fetchCategoryList'; +import { fetchGoodsCaseView } from '../../../services/home/home'; +import Toast from 'tdesign-miniprogram/toast/index'; + +const initFilters = { + overall: 1, + sorts: '', + layout: 0, +}; + +Page({ + data: { + goodsList: [], + layout: 0, + sorts: '', + overall: 1, + show: false, + minVal: '', + maxVal: '', + keywords: '', + page: 1, + pageSize: 10, + total: 0, + tabValue: 0, + filter: initFilters, + hasLoaded: false, + loadMoreStatus: 0, + loading: true, + tabList: [], + caseView: wx.getStorageSync('goodsCaseView') === true, // 案例展示:标题改为软件案例,不显示价格和购物车 + }, + + handleFilterChange(e) { + const { layout, overall, sorts } = e.detail; + this.setData({ + layout, + sorts, + page: 1, + overall, + loadMoreStatus: 0, + }); + this.loadGoodsList(true); + }, + tabChangeHandle(e) { + const tabValue = e.detail.value; + this.setData({ + tabValue, + }); + this.loadGoodsList(true); + }, + //搜索 + onChangeInput(e) { + const { value } = e.detail; + this.setData({ + keywords: value + }); + if (!value) { + this.loadGoodsList(true); + } + }, + onChangeSearch(e) { + const { value } = e.detail; + if (value !== undefined) { + this.setData({ + keywords: value + }); + } + console.log(e) + console.log(value) + this.loadGoodsList(true); + }, + navToSearchPage() { + wx.navigateTo({ + url: '/pages/goods/search/index' + }); + }, + + async init() { + try { + const result = await getCategoryList(); + this.setData({ + tabList: result, + }); + } catch (error) { + console.error('err:', error); + } + }, + + // 产品列表 + async loadGoodsList(reset = false) { + if (!reset && (this._loading || this.data.loadMoreStatus !== 0)) { + return; + } + const seq = (this._listSeq || 0) + 1; + this._listSeq = seq; + this._loading = true; + const page = reset ? 1 : this.data.page; + const { pageSize, keywords, tabValue } = this.data; + this.setData({ + loadMoreStatus: 1, + loading: true, + }); + try { + const result = await fetchGoodsList({ + page, + categoryId: tabValue, + pageSize, + keywords, + }); + if (seq !== this._listSeq) { + return; + } + if (!result || result.code !== 1 || !result.data) { + this.setData({ + loading: false, + hasLoaded: true, + loadMoreStatus: 0, + }); + wx.showToast({ + title: (result && result.msg) || '查询失败,请稍候重试', + icon: 'none', + }); + return; + } + const { spuList = [], totalCount = 0 } = result.data; + if (totalCount === 0 && reset) { + this.setData({ + emptyInfo: { + tip: '抱歉,未找到相关商品', + }, + total: totalCount, + hasLoaded: true, + loadMoreStatus: 2, + loading: false, + goodsList: [], + page: 1, + }); + return; + } + const start = reset ? 0 : this.data.goodsList.length; + const loaded = start + spuList.length; + const patch = { + total: totalCount, + page: page + 1, + loadMoreStatus: loaded >= totalCount ? 2 : 0, + hasLoaded: true, + loading: false, + }; + if (reset) { + patch.goodsList = spuList; + } else { + spuList.forEach((item, i) => { + patch[`goodsList[${start + i}]`] = item; + }); + } + this.setData(patch); + } catch (error) { + if (seq !== this._listSeq) { + return; + } + console.error('loadGoodsList failed:', error); + this.setData({ + loading: false, + hasLoaded: true, + loadMoreStatus: 0, + }); + } finally { + if (seq === this._listSeq) { + this._loading = false; + } + } + }, + + + onLoad() { + this.applyCaseView(); + this.init(); + this.loadGoodsList(true); + }, + + onShow() { + // 自定义 tabBar 每个 Tab 页各有一份实例,切过来后要按当前路由重设选中项 + const tabBar = typeof this.getTabBar === 'function' && this.getTabBar(); + if (tabBar) { + tabBar.init(); + } + }, + + async applyCaseView() { + if (this.data.caseView) { + wx.setNavigationBarTitle({ title: '软件案例' }); + } + const caseView = await fetchGoodsCaseView(); + this.setData({ caseView }); + wx.setNavigationBarTitle({ title: caseView ? '软件案例' : '软件列表' }); + }, + + onReachBottom() { + if (this._loading || this.data.loadMoreStatus !== 0) { + return; + } + const { goodsList, total } = this.data; + if (goodsList.length >= total) { + this.setData({ + loadMoreStatus: 2, + }); + return; + } + this.loadGoodsList(false); + }, + + tagClickHandle() { + Toast({ + context: this, + selector: '#t-toast', + message: '点击标签', + }); + }, + + gotoGoodsDetail(e) { + console.log(e); + const { index } = e.detail; + const { spuId } = this.data.goodsList[index]; + wx.navigateTo({ + url: `/pages/goods/details/index?spuId=${spuId}`, + }); + }, + + showFilterPopup() { + this.setData({ + show: true, + }); + }, + + showFilterPopupClose() { + this.setData({ + show: false, + }); + }, + + onMinValAction(e) { + const { value } = e.detail; + this.setData({ minVal: value }); + }, + + onMaxValAction(e) { + const { value } = e.detail; + this.setData({ maxVal: value }); + }, + + reset() { + this.setData({ minVal: '', maxVal: '' }); + }, + + confirm() { + let message = '' + if (message) { + Toast({ + context: this, + selector: '#t-toast', + message, + }); + } + this.setData( + { + show: false, + page: 1, + goodsList: [], + loadMoreStatus: 0, + }, + () => { + this.loadGoodsList(); + }, + ); + }, +}); diff --git a/frontend/miniapp/pages/goods/list/index.json b/frontend/miniapp/pages/goods/list/index.json new file mode 100644 index 0000000..1d77ba2 --- /dev/null +++ b/frontend/miniapp/pages/goods/list/index.json @@ -0,0 +1,16 @@ +{ + "navigationBarTitleText": "软件列表", + "usingComponents": { + "t-tabs": "tdesign-miniprogram/tabs/tabs", + "t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel", + "t-search": "tdesign-miniprogram/search/search", + "t-input": "tdesign-miniprogram/input/input", + "t-empty": "tdesign-miniprogram/empty/empty", + "t-toast": "tdesign-miniprogram/toast/toast", + "goods-list": "/components/goods-list/index", + "filter": "/components/filter/index", + "t-icon": "tdesign-miniprogram/icon/icon", + "filter-popup": "/components/filter-popup/index", + "load-more": "/components/load-more/index" + } +} \ No newline at end of file diff --git a/frontend/miniapp/pages/goods/list/index.wxml b/frontend/miniapp/pages/goods/list/index.wxml new file mode 100644 index 0000000..ed9f3de --- /dev/null +++ b/frontend/miniapp/pages/goods/list/index.wxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/miniapp/pages/goods/list/index.wxss b/frontend/miniapp/pages/goods/list/index.wxss new file mode 100644 index 0000000..6529ead --- /dev/null +++ b/frontend/miniapp/pages/goods/list/index.wxss @@ -0,0 +1,91 @@ +page { + background-color: var(--bg-page); +} + +.goods-list-container { + display: block; + min-height: 100vh; + background-color: var(--bg-page); +} + +/* 顶部搜索 + 分类:吸顶固定,滚动时不遮挡内容 */ +.goods-list-container .list-header { + position: sticky; + top: 0; + z-index: 20; + background-color: var(--bg-card); + box-shadow: 0 4rpx 16rpx rgba(29, 33, 41, 0.03); +} + +.goods-list-container .search { + padding: 16rpx 24rpx 8rpx; + background-color: var(--bg-card); +} + +.goods-list-container .t-class__input-container { + height: 72rpx !important; + border-radius: var(--radius-round) !important; + background: var(--bg-grey); +} + +.goods-list-container .t-search__left-icon { + display: flex; + align-items: center; +} + +.goods-list-container .t-search__input { + font-size: 28rpx !important; + color: var(--text-primary) !important; +} + +.goods-list-container .home-page-tabs { + padding: 0 8rpx; + background: var(--bg-card); + border-radius: 0; +} + +.goods-list-container .tabs-external__active { + color: var(--brand) !important; + font-size: 32rpx; + font-weight: 600; +} + +.goods-list-container .tabs-external__track { + width: 48rpx !important; + height: 6rpx !important; + background-color: var(--brand) !important; + border-radius: var(--radius-round) !important; +} + +.goods-list-container .t-tabs.t-tabs--top .t-tabs__scroll { + border-bottom: none; +} + +.goods-list-container .t-tabs.t-tabs--top .t-tabs__item { + height: 84rpx !important; +} + +.goods-list-container .category-goods-list { + padding: 24rpx 24rpx 0; + background-color: var(--bg-page); +} + +.goods-list-container .t-image__mask { + display: flex !important; +} + +.goods-list-container .empty-wrap { + display: flex; + align-items: center; + justify-content: center; + height: 640rpx; +} + +.goods-list-container .empty-wrap .empty-tips .empty-content .content-text { + margin-top: 40rpx; +} + +.safe-bottom { + width: 100%; + min-height: 150rpx; +} diff --git a/frontend/miniapp/pages/goods/result/index.js b/frontend/miniapp/pages/goods/result/index.js index 52c979b..8cbe4f8 100644 --- a/frontend/miniapp/pages/goods/result/index.js +++ b/frontend/miniapp/pages/goods/result/index.js @@ -159,6 +159,14 @@ Page({ this.init(false); }, + gotoGoodsDetail(e) { + const { index } = e.detail; + const { spuId } = this.data.goodsList[index]; + wx.navigateTo({ + url: `/pages/goods/details/index?spuId=${spuId}`, + }); + }, + handleAddCart() { Toast({ context: this, diff --git a/frontend/miniapp/pages/goods/result/index.wxml b/frontend/miniapp/pages/goods/result/index.wxml index 7ca3113..97bb7bb 100644 --- a/frontend/miniapp/pages/goods/result/index.wxml +++ b/frontend/miniapp/pages/goods/result/index.wxml @@ -18,7 +18,7 @@ - + diff --git a/frontend/miniapp/pages/message/detail/index.js b/frontend/miniapp/pages/message/detail/index.js index 4182f06..62e4a33 100644 --- a/frontend/miniapp/pages/message/detail/index.js +++ b/frontend/miniapp/pages/message/detail/index.js @@ -5,6 +5,7 @@ import { fetchMessageDetail, messageSetRead } from '../../../services/message/fe // navigateTo 无法跳转 tab 页,必须用 switchTab;而 switchTab 不支持带参数 const TAB_PAGES = [ 'pages/home/home', + 'pages/goods/list/index', 'pages/message/list/index', 'pages/usercenter/index', ]; diff --git a/frontend/miniapp/pages/order/after-service-detail/index.js b/frontend/miniapp/pages/order/after-service-detail/index.js index fd0f225..b2b6e1e 100644 --- a/frontend/miniapp/pages/order/after-service-detail/index.js +++ b/frontend/miniapp/pages/order/after-service-detail/index.js @@ -144,6 +144,12 @@ Page({ }; }, + onGoodsCardTap(e) { + const { index } = e.currentTarget.dataset; + const goods = this.data.serviceRaw.rightsItem[index]; + wx.navigateTo({ url: `/pages/goods/details/index?skuId=${goods.skuId}` }); + }, + onProofTap(e) { if (this.data.gallery.show) { this.setData({ diff --git a/frontend/miniapp/pages/order/after-service-detail/index.wxml b/frontend/miniapp/pages/order/after-service-detail/index.wxml index 2f05d52..1ef66a0 100644 --- a/frontend/miniapp/pages/order/after-service-detail/index.wxml +++ b/frontend/miniapp/pages/order/after-service-detail/index.wxml @@ -89,6 +89,8 @@ wx:for-item="goods" goods="{{goods}}" no-top-line + bindtap="onGoodsCardTap" + data-index="{{index}}" > - + diff --git a/frontend/miniapp/pages/promotion-detail/index.js b/frontend/miniapp/pages/promotion-detail/index.js index ec50b5e..dd6614e 100644 --- a/frontend/miniapp/pages/promotion-detail/index.js +++ b/frontend/miniapp/pages/promotion-detail/index.js @@ -33,6 +33,12 @@ Page({ ); }, + goodClickHandle(e) { + const { index } = e.detail; + const { spuId } = this.data.list[index]; + wx.navigateTo({ url: `/pages/goods/details/index?spuId=${spuId}` }); + }, + cardClickHandle() { Toast({ context: this, diff --git a/frontend/miniapp/pages/promotion-detail/index.wxml b/frontend/miniapp/pages/promotion-detail/index.wxml index f167eee..e1ed69b 100644 --- a/frontend/miniapp/pages/promotion-detail/index.wxml +++ b/frontend/miniapp/pages/promotion-detail/index.wxml @@ -66,6 +66,7 @@ diff --git a/frontend/miniapp/project.config.json b/frontend/miniapp/project.config.json index cacd009..65e0876 100644 --- a/frontend/miniapp/project.config.json +++ b/frontend/miniapp/project.config.json @@ -65,6 +65,18 @@ "query": "", "scene": null }, + { + "name": "示例页-商品列表", + "pathName": "pages/goods/list/index", + "query": "", + "scene": null + }, + { + "name": "示例页-商品详情", + "pathName": "pages/goods/details/index", + "query": "", + "scene": null + }, { "name": "示例页-商品分类", "pathName": "pages/goods/category/index",