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 @@
+
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 @@
+
+
+
+
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 @@
+
+
+
+
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 || '请选择规格'}}
+
+
+
+
+
+
+
+
+
+
+
+ 详情介绍
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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}}"
>