From 4cc5d8bf92ea77d26853055dee4ae3ca8bafb185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=83=E8=B4=A7?= <252048765@qq.com> Date: Sun, 6 Jul 2025 08:49:22 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E6=A8=A1=E5=9D=97=EF=BC=8C=E6=96=B0=E5=A2=9E=E4=BC=9A?= =?UTF-8?q?=E5=91=98=E4=B8=AD=E5=BF=83=E7=9A=84=E4=BC=9A=E5=91=98=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=9A=84=E8=AE=A2=E5=8D=95=E7=AE=A1=E7=90=86=EF=BC=8C?= =?UTF-8?q?=E5=94=AE=E5=90=8E=E7=AE=A1=E7=90=86=EF=BC=8C=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=EF=BC=8C=E4=BC=98=E6=83=A0=E5=88=B8=EF=BC=8C?= =?UTF-8?q?=E6=8E=A8=E5=B9=BF=E7=94=A8=E6=88=B7=E7=9A=84=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Tinyflow/ui/index.css | 506 +++++++++++------- .../src/components/summary-card/index.ts | 2 + .../components/summary-card/summary-card.vue | 57 ++ .../src/components/summary-card/typing.ts | 11 + apps/web-ele/src/views/mall/home/index.vue | 177 ++++++ .../src/views/mall/product/brand/data.ts | 132 +++++ .../src/views/mall/product/brand/index.vue | 125 +++++ .../views/mall/product/brand/modules/form.vue | 83 +++ .../src/views/mall/product/category/data.ts | 139 +++++ .../src/views/mall/product/category/index.vue | 187 +++++++ .../mall/product/category/modules/form.vue | 89 +++ .../src/views/mall/product/comment/data.ts | 202 +++++++ .../src/views/mall/product/comment/index.vue | 159 ++++++ .../mall/product/comment/modules/form.vue | 83 +++ .../src/views/mall/product/property/data.ts | 176 ++++++ .../src/views/mall/product/property/index.vue | 35 ++ .../property/modules/property-form.vue | 88 +++ .../property/modules/property-grid.vue | 140 +++++ .../product/property/modules/value-form.vue | 100 ++++ .../product/property/modules/value-grid.vue | 149 ++++++ .../src/views/mall/product/spu/data.ts | 117 ++++ .../src/views/mall/product/spu/index.vue | 349 ++++++++++++ .../views/mall/product/spu/modules/detail.vue | 3 + .../views/mall/product/spu/modules/form.vue | 3 + .../mall/promotion/article/category/data.ts | 135 +++++ .../mall/promotion/article/category/index.vue | 128 +++++ .../article/category/modules/form.vue | 90 ++++ .../src/views/mall/promotion/article/data.ts | 210 ++++++++ .../views/mall/promotion/article/index.vue | 125 +++++ .../mall/promotion/article/modules/form.vue | 87 +++ .../src/views/mall/promotion/banner/data.ts | 175 ++++++ .../src/views/mall/promotion/banner/index.vue | 125 +++++ .../mall/promotion/banner/modules/form.vue | 87 +++ .../mall/promotion/bargain/activity/data.ts | 262 +++++++++ .../mall/promotion/bargain/activity/index.vue | 173 ++++++ .../bargain/activity/modules/form.vue | 92 ++++ .../mall/promotion/bargain/record/data.ts | 161 ++++++ .../mall/promotion/bargain/record/index.vue | 84 +++ .../promotion/bargain/record/modules/list.vue | 67 +++ .../promotion/combination/activity/data.ts | 238 ++++++++ .../promotion/combination/activity/index.vue | 177 ++++++ .../combination/activity/modules/form.vue | 93 ++++ .../mall/promotion/combination/record/data.ts | 177 ++++++ .../promotion/combination/record/index.vue | 82 +++ .../combination/record/modules/list.vue | 63 +++ .../src/views/mall/promotion/coupon/data.ts | 129 +++++ .../views/mall/promotion/coupon/formatter.ts | 65 +++ .../src/views/mall/promotion/coupon/index.vue | 129 +++++ .../mall/promotion/coupon/template/data.ts | 252 +++++++++ .../mall/promotion/coupon/template/index.vue | 185 +++++++ .../coupon/template/modules/form.vue | 89 +++ .../mall/promotion/discountActivity/data.ts | 159 ++++++ .../mall/promotion/discountActivity/index.vue | 173 ++++++ .../discountActivity/modules/form.vue | 98 ++++ .../src/views/mall/promotion/diy/page/data.ts | 109 ++++ .../views/mall/promotion/diy/page/index.vue | 143 +++++ .../mall/promotion/diy/page/modules/form.vue | 92 ++++ .../views/mall/promotion/diy/template/data.ts | 120 +++++ .../mall/promotion/diy/template/index.vue | 170 ++++++ .../promotion/diy/template/modules/form.vue | 99 ++++ .../src/views/mall/promotion/kefu/index.vue | 26 + .../mall/promotion/point/activity/data.ts | 140 +++++ .../mall/promotion/point/activity/index.vue | 162 ++++++ .../promotion/point/activity/modules/form.vue | 107 ++++ .../mall/promotion/rewardActivity/data.ts | 166 ++++++ .../mall/promotion/rewardActivity/index.vue | 173 ++++++ .../promotion/rewardActivity/modules/form.vue | 105 ++++ .../mall/promotion/seckill/activity/data.ts | 126 +++++ .../promotion/seckill/activity/formatter.ts | 34 ++ .../mall/promotion/seckill/activity/index.vue | 193 +++++++ .../seckill/activity/modules/form.vue | 121 +++++ .../mall/promotion/seckill/config/data.ts | 151 ++++++ .../mall/promotion/seckill/config/index.vue | 157 ++++++ .../promotion/seckill/config/modules/form.vue | 90 ++++ .../views/mall/statistics/member/index.vue | 34 ++ .../views/mall/statistics/product/index.vue | 34 ++ .../src/views/mall/statistics/trade/index.vue | 34 ++ .../src/views/mall/trade/afterSale/data.ts | 140 +++++ .../src/views/mall/trade/afterSale/index.vue | 120 +++++ .../views/mall/trade/brokerage/record/data.ts | 135 +++++ .../mall/trade/brokerage/record/index.vue | 57 ++ .../views/mall/trade/brokerage/user/data.ts | 140 +++++ .../views/mall/trade/brokerage/user/index.vue | 221 ++++++++ .../user/modules/order-list-modal.vue | 185 +++++++ .../user/modules/user-create-form.vue | 181 +++++++ .../user/modules/user-list-modal.vue | 160 ++++++ .../user/modules/user-update-form.vue | 136 +++++ .../mall/trade/brokerage/withdraw/data.ts | 145 +++++ .../mall/trade/brokerage/withdraw/index.vue | 195 +++++++ .../src/views/mall/trade/config/data.ts | 243 +++++++++ .../src/views/mall/trade/config/index.vue | 98 ++++ .../views/mall/trade/delivery/express/data.ts | 130 +++++ .../mall/trade/delivery/express/index.vue | 141 +++++ .../trade/delivery/express/modules/form.vue | 89 +++ .../trade/delivery/expressTemplate/data.ts | 102 ++++ .../trade/delivery/expressTemplate/index.vue | 128 +++++ .../delivery/expressTemplate/modules/form.vue | 91 ++++ .../mall/trade/delivery/pickUpOrder/data.ts | 127 +++++ .../mall/trade/delivery/pickUpOrder/index.vue | 241 +++++++++ .../mall/trade/delivery/pickUpStore/data.ts | 245 +++++++++ .../mall/trade/delivery/pickUpStore/index.vue | 145 +++++ .../pickUpStore/modules/bind-form.vue | 87 +++ .../delivery/pickUpStore/modules/form.vue | 89 +++ .../src/views/mall/trade/order/data.ts | 214 ++++++++ .../src/views/mall/trade/order/index.vue | 217 ++++++++ .../trade/order/modules/delevery-form.vue | 131 +++++ .../user/components/user-after-sale-list.vue | 147 +++++ .../user/components/user-brokerage-list.vue | 139 +++++ .../user/components/user-coupon-list.vue | 181 +++++++ .../user/components/user-favorite-list.vue | 92 ++++ .../user/components/user-order-list.vue | 270 ++++++++++ .../src/views/member/user/modules/detail.vue | 15 +- .../src/components/json-viewer/style.scss | 2 +- .../src/components/resize/resize.vue | 2 +- .../src/widgets/theme-toggle/theme-button.vue | 4 +- 115 files changed, 14803 insertions(+), 190 deletions(-) create mode 100644 apps/web-ele/src/components/summary-card/index.ts create mode 100644 apps/web-ele/src/components/summary-card/summary-card.vue create mode 100644 apps/web-ele/src/components/summary-card/typing.ts create mode 100644 apps/web-ele/src/views/mall/home/index.vue create mode 100644 apps/web-ele/src/views/mall/product/brand/data.ts create mode 100644 apps/web-ele/src/views/mall/product/brand/index.vue create mode 100644 apps/web-ele/src/views/mall/product/brand/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/product/category/data.ts create mode 100644 apps/web-ele/src/views/mall/product/category/index.vue create mode 100644 apps/web-ele/src/views/mall/product/category/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/product/comment/data.ts create mode 100644 apps/web-ele/src/views/mall/product/comment/index.vue create mode 100644 apps/web-ele/src/views/mall/product/comment/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/product/property/data.ts create mode 100644 apps/web-ele/src/views/mall/product/property/index.vue create mode 100644 apps/web-ele/src/views/mall/product/property/modules/property-form.vue create mode 100644 apps/web-ele/src/views/mall/product/property/modules/property-grid.vue create mode 100644 apps/web-ele/src/views/mall/product/property/modules/value-form.vue create mode 100644 apps/web-ele/src/views/mall/product/property/modules/value-grid.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/data.ts create mode 100644 apps/web-ele/src/views/mall/product/spu/index.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/modules/detail.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/article/category/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/article/category/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/article/category/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/article/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/article/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/article/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/banner/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/banner/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/banner/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/bargain/activity/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/bargain/activity/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/bargain/activity/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/bargain/record/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/bargain/record/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/bargain/record/modules/list.vue create mode 100644 apps/web-ele/src/views/mall/promotion/combination/activity/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/combination/activity/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/combination/activity/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/combination/record/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/combination/record/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/combination/record/modules/list.vue create mode 100644 apps/web-ele/src/views/mall/promotion/coupon/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/coupon/formatter.ts create mode 100644 apps/web-ele/src/views/mall/promotion/coupon/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/coupon/template/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/coupon/template/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/coupon/template/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/discountActivity/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/discountActivity/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/discountActivity/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/diy/page/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/diy/page/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/diy/page/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/diy/template/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/diy/template/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/diy/template/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/kefu/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/point/activity/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/point/activity/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/point/activity/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/rewardActivity/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/rewardActivity/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/rewardActivity/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/activity/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/activity/formatter.ts create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/activity/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/activity/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/config/data.ts create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/config/index.vue create mode 100644 apps/web-ele/src/views/mall/promotion/seckill/config/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/statistics/member/index.vue create mode 100644 apps/web-ele/src/views/mall/statistics/product/index.vue create mode 100644 apps/web-ele/src/views/mall/statistics/trade/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/afterSale/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/afterSale/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/record/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/record/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/user/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/user/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-create-form.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-update-form.vue create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/withdraw/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/brokerage/withdraw/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/config/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/config/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/express/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/delivery/express/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/express/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/expressTemplate/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/delivery/expressTemplate/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/expressTemplate/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/pickUpStore/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/delivery/pickUpStore/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue create mode 100644 apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/form.vue create mode 100644 apps/web-ele/src/views/mall/trade/order/data.ts create mode 100644 apps/web-ele/src/views/mall/trade/order/index.vue create mode 100644 apps/web-ele/src/views/mall/trade/order/modules/delevery-form.vue create mode 100644 apps/web-ele/src/views/member/user/components/user-after-sale-list.vue create mode 100644 apps/web-ele/src/views/member/user/components/user-brokerage-list.vue create mode 100644 apps/web-ele/src/views/member/user/components/user-coupon-list.vue create mode 100644 apps/web-ele/src/views/member/user/components/user-favorite-list.vue create mode 100644 apps/web-ele/src/views/member/user/components/user-order-list.vue diff --git a/apps/web-antd/src/components/Tinyflow/ui/index.css b/apps/web-antd/src/components/Tinyflow/ui/index.css index 1e126fa75..f5a473a13 100644 --- a/apps/web-antd/src/components/Tinyflow/ui/index.css +++ b/apps/web-antd/src/components/Tinyflow/ui/index.css @@ -1,13 +1,12 @@ .svelte-flow { - direction: ltr; --xy-edge-stroke-default: #b1b1b7; --xy-edge-stroke-width-default: 1; --xy-edge-stroke-selected-default: #555; --xy-connectionline-stroke-default: #b1b1b7; --xy-connectionline-stroke-width-default: 1; - --xy-attribution-background-color-default: rgba(255, 255, 255, 0.5); + --xy-attribution-background-color-default: rgb(255 255 255 / 50%); --xy-minimap-background-color-default: #fff; - --xy-minimap-mask-background-color-default: rgb(240, 240, 240, 0.6); + --xy-minimap-mask-background-color-default: rgb(240 240 240 / 60%); --xy-minimap-mask-stroke-color-default: transparent; --xy-minimap-mask-stroke-width-default: 1; --xy-minimap-node-background-color-default: #e2e2e2; @@ -17,40 +16,43 @@ --xy-background-pattern-dots-color-default: #91919a; --xy-background-pattern-lines-color-default: #eee; --xy-background-pattern-cross-color-default: #e2e2e2; - background-color: var( - --xy-background-color, - var(--xy-background-color-default) - ); --xy-node-color-default: inherit; --xy-node-border-default: 1px solid #1a192b; --xy-node-background-color-default: #fff; - --xy-node-group-background-color-default: rgba(240, 240, 240, 0.25); - --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, 0.08); + --xy-node-group-background-color-default: rgb(240 240 240 / 25%); + --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgb(0 0 0 / 8%); --xy-node-boxshadow-selected-default: 0 0 0 0.5px #1a192b; --xy-node-border-radius-default: 3px; --xy-handle-background-color-default: #1a192b; --xy-handle-border-color-default: #fff; - --xy-selection-background-color-default: rgba(0, 89, 220, 0.08); - --xy-selection-border-default: 1px dotted rgba(0, 89, 220, 0.8); + --xy-selection-background-color-default: rgb(0 89 220 / 8%); + --xy-selection-border-default: 1px dotted rgb(0 89 220 / 80%); --xy-controls-button-background-color-default: #fefefe; --xy-controls-button-background-color-hover-default: #f4f4f4; --xy-controls-button-color-default: inherit; --xy-controls-button-color-hover-default: inherit; --xy-controls-button-border-color-default: #eee; - --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); - --xy-edge-label-background-color-default: #ffffff; + --xy-controls-box-shadow-default: 0 0 2px 1px rgb(0 0 0 / 8%); + --xy-edge-label-background-color-default: #fff; --xy-edge-label-color-default: inherit; --xy-resize-background-color-default: #3367d9; + + direction: ltr; + background-color: var( + --xy-background-color, + var(--xy-background-color-default) + ); } + .svelte-flow.dark { --xy-edge-stroke-default: #3e3e3e; --xy-edge-stroke-width-default: 1; --xy-edge-stroke-selected-default: #727272; --xy-connectionline-stroke-default: #b1b1b7; --xy-connectionline-stroke-width-default: 1; - --xy-attribution-background-color-default: rgba(150, 150, 150, 0.25); + --xy-attribution-background-color-default: rgb(150 150 150 / 25%); --xy-minimap-background-color-default: #141414; - --xy-minimap-mask-background-color-default: rgb(60, 60, 60, 0.6); + --xy-minimap-mask-background-color-default: rgb(60 60 60 / 60%); --xy-minimap-mask-stroke-color-default: transparent; --xy-minimap-mask-stroke-width-default: 1; --xy-minimap-node-background-color-default: #2b2b2b; @@ -63,73 +65,86 @@ --xy-node-color-default: #f8f8f8; --xy-node-border-default: 1px solid #3c3c3c; --xy-node-background-color-default: #1e1e1e; - --xy-node-group-background-color-default: rgba(240, 240, 240, 0.25); - --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, 0.08); + --xy-node-group-background-color-default: rgb(240 240 240 / 25%); + --xy-node-boxshadow-hover-default: 0 1px 4px 1px rgb(255 255 255 / 8%); --xy-node-boxshadow-selected-default: 0 0 0 0.5px #999; --xy-handle-background-color-default: #bebebe; --xy-handle-border-color-default: #1e1e1e; - --xy-selection-background-color-default: rgba(200, 200, 220, 0.08); - --xy-selection-border-default: 1px dotted rgba(200, 200, 220, 0.8); + --xy-selection-background-color-default: rgb(200 200 220 / 8%); + --xy-selection-border-default: 1px dotted rgb(200 200 220 / 80%); --xy-controls-button-background-color-default: #2b2b2b; --xy-controls-button-background-color-hover-default: #3e3e3e; --xy-controls-button-color-default: #f8f8f8; --xy-controls-button-color-hover-default: #fff; --xy-controls-button-border-color-default: #5b5b5b; - --xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, 0.08); + --xy-controls-box-shadow-default: 0 0 2px 1px rgb(0 0 0 / 8%); --xy-edge-label-background-color-default: #141414; --xy-edge-label-color-default: #f8f8f8; } + .svelte-flow__background { + z-index: -1; + pointer-events: none; background-color: var( --xy-background-color, var(--xy-background-color-props, var(--xy-background-color-default)) ); - pointer-events: none; - z-index: -1; } + .svelte-flow__container { position: absolute; - width: 100%; - height: 100%; top: 0; left: 0; + width: 100%; + height: 100%; } + .svelte-flow__pane { z-index: 1; } + .svelte-flow__pane.draggable { cursor: grab; } + .svelte-flow__pane.dragging { cursor: grabbing; } + .svelte-flow__pane.selection { cursor: pointer; } + .svelte-flow__viewport { - transform-origin: 0 0; z-index: 2; pointer-events: none; + transform-origin: 0 0; } + .svelte-flow__renderer { z-index: 4; } + .svelte-flow__selection { z-index: 6; } + .svelte-flow__nodesselection-rect:focus, .svelte-flow__nodesselection-rect:focus-visible { outline: none; } + .svelte-flow__edge-path { + fill: none; stroke: var(--xy-edge-stroke, var(--xy-edge-stroke-default)); stroke-width: var( --xy-edge-stroke-width, var(--xy-edge-stroke-width-default) ); - fill: none; } + .svelte-flow__connection-path { + fill: none; stroke: var( --xy-connectionline-stroke, var(--xy-connectionline-stroke-default) @@ -138,38 +153,46 @@ --xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default) ); - fill: none; } + .svelte-flow .svelte-flow__edges { position: absolute; } + .svelte-flow .svelte-flow__edges svg { - overflow: visible; position: absolute; + overflow: visible; pointer-events: none; } + .svelte-flow__edge { - pointer-events: visibleStroke; + pointer-events: visiblestroke; } + .svelte-flow__edge.selectable { cursor: pointer; } + .svelte-flow__edge.animated path { stroke-dasharray: 5; animation: dashdraw 0.5s linear infinite; } + .svelte-flow__edge.animated path.svelte-flow__edge-interaction { stroke-dasharray: none; animation: none; } + .svelte-flow__edge.inactive { pointer-events: none; } + .svelte-flow__edge.selected, .svelte-flow__edge:focus, .svelte-flow__edge:focus-visible { outline: none; } + .svelte-flow__edge.selected .svelte-flow__edge-path, .svelte-flow__edge.selectable:focus .svelte-flow__edge-path, .svelte-flow__edge.selectable:focus-visible .svelte-flow__edge-path { @@ -178,68 +201,77 @@ var(--xy-edge-stroke-selected-default) ); } + .svelte-flow__edge-textwrapper { pointer-events: all; } + .svelte-flow__edge .svelte-flow__edge-text { pointer-events: none; - -webkit-user-select: none; - -moz-user-select: none; user-select: none; } + .svelte-flow__connection { pointer-events: none; } + .svelte-flow__connection .animated { stroke-dasharray: 5; animation: dashdraw 0.5s linear infinite; } + svg.svelte-flow__connectionline { + position: absolute; z-index: 1001; overflow: visible; - position: absolute; } + .svelte-flow__nodes { pointer-events: none; transform-origin: 0 0; } + .svelte-flow__node { position: absolute; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - pointer-events: all; - transform-origin: 0 0; box-sizing: border-box; + pointer-events: all; cursor: default; + user-select: none; + transform-origin: 0 0; } + .svelte-flow__node.selectable { cursor: pointer; } + .svelte-flow__node.draggable { - cursor: grab; pointer-events: all; + cursor: grab; } + .svelte-flow__node.draggable.dragging { cursor: grabbing; } + .svelte-flow__nodesselection { z-index: 3; - transform-origin: left top; pointer-events: none; + transform-origin: left top; } + .svelte-flow__nodesselection-rect { position: absolute; pointer-events: all; cursor: grab; } + .svelte-flow__handle { position: absolute; - pointer-events: none; - min-width: 5px; - min-height: 5px; width: 6px; + min-width: 5px; height: 6px; + min-height: 5px; + pointer-events: none; background-color: var( --xy-handle-background-color, var(--xy-handle-background-color-default) @@ -248,98 +280,113 @@ svg.svelte-flow__connectionline { var(--xy-handle-border-color, var(--xy-handle-border-color-default)); border-radius: 100%; } + .svelte-flow__handle.connectingfrom { pointer-events: all; } + .svelte-flow__handle.connectionindicator { pointer-events: all; cursor: crosshair; } + .svelte-flow__handle-bottom { top: auto; - left: 50%; bottom: 0; + left: 50%; transform: translate(-50%, 50%); } + .svelte-flow__handle-top { top: 0; left: 50%; transform: translate(-50%, -50%); } + .svelte-flow__handle-left { top: 50%; left: 0; transform: translate(-50%, -50%); } + .svelte-flow__handle-right { top: 50%; right: 0; transform: translate(50%, -50%); } + .svelte-flow__edgeupdater { - cursor: move; pointer-events: all; + cursor: move; } + .svelte-flow__panel { position: absolute; z-index: 5; margin: 15px; } + .svelte-flow__panel.top { top: 0; } + .svelte-flow__panel.bottom { bottom: 0; } + .svelte-flow__panel.left { left: 0; } + .svelte-flow__panel.right { right: 0; } + .svelte-flow__panel.center { left: 50%; transform: translate(-50%); } + .svelte-flow__attribution { + padding: 2px 3px; + margin: 0; font-size: 10px; background: var( --xy-attribution-background-color, var(--xy-attribution-background-color-default) ); - padding: 2px 3px; - margin: 0; } + .svelte-flow__attribution a { - text-decoration: none; color: #999; + text-decoration: none; } + @keyframes dashdraw { 0% { stroke-dashoffset: 10; } } + .svelte-flow__edgelabel-renderer { position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; pointer-events: none; - -webkit-user-select: none; - -moz-user-select: none; user-select: none; - left: 0; - top: 0; } + .svelte-flow__viewport-portal { position: absolute; + top: 0; + left: 0; width: 100%; height: 100%; - left: 0; - top: 0; - -webkit-user-select: none; - -moz-user-select: none; user-select: none; } + .svelte-flow__minimap { background: var( --xy-minimap-background-color-props, @@ -349,9 +396,11 @@ svg.svelte-flow__connectionline { ) ); } + .svelte-flow__minimap-svg { display: block; } + .svelte-flow__minimap-mask { fill: var( --xy-minimap-mask-background-color-props, @@ -375,6 +424,7 @@ svg.svelte-flow__connectionline { ) ); } + .svelte-flow__minimap-node { fill: var( --xy-minimap-node-background-color-props, @@ -398,6 +448,7 @@ svg.svelte-flow__connectionline { ) ); } + .svelte-flow__background-pattern.dots { fill: var( --xy-background-pattern-color-props, @@ -407,6 +458,7 @@ svg.svelte-flow__connectionline { ) ); } + .svelte-flow__background-pattern.lines { stroke: var( --xy-background-pattern-color-props, @@ -416,6 +468,7 @@ svg.svelte-flow__connectionline { ) ); } + .svelte-flow__background-pattern.cross { stroke: var( --xy-background-pattern-color-props, @@ -425,6 +478,7 @@ svg.svelte-flow__connectionline { ) ); } + .svelte-flow__controls { display: flex; flex-direction: column; @@ -433,21 +487,29 @@ svg.svelte-flow__connectionline { var(--xy-controls-box-shadow-default) ); } + .svelte-flow__controls.horizontal { flex-direction: row; } + .svelte-flow__controls-button { display: flex; - justify-content: center; align-items: center; - height: 26px; + justify-content: center; width: 26px; + height: 26px; padding: 4px; - border: none; + color: var( + --xy-controls-button-color-props, + var(--xy-controls-button-color, var(--xy-controls-button-color-default)) + ); + cursor: pointer; + user-select: none; background: var( --xy-controls-button-background-color, var(--xy-controls-button-background-color-default) ); + border: none; border-bottom: 1px solid var( --xy-controls-button-border-color-props, @@ -456,50 +518,48 @@ svg.svelte-flow__connectionline { var(--xy-controls-button-border-color-default) ) ); - color: var( - --xy-controls-button-color-props, - var(--xy-controls-button-color, var(--xy-controls-button-color-default)) - ); - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; } + .svelte-flow__controls-button svg { width: 100%; max-width: 12px; max-height: 12px; - fill: currentColor; + fill: currentcolor; } + .svelte-flow__edge.updating .svelte-flow__edge-path { stroke: #777; } + .svelte-flow__edge-text { font-size: 10px; } + .svelte-flow__node.selectable:focus, .svelte-flow__node.selectable:focus-visible { outline: none; } + .svelte-flow__node-input, .svelte-flow__node-default, .svelte-flow__node-output, .svelte-flow__node-group { - padding: 10px; - border-radius: var( - --xy-node-border-radius, - var(--xy-node-border-radius-default) - ); width: 150px; + padding: 10px; font-size: 12px; color: var(--xy-node-color, var(--xy-node-color-default)); text-align: center; - border: var(--xy-node-border, var(--xy-node-border-default)); background-color: var( --xy-node-background-color, var(--xy-node-background-color-default) ); + border: var(--xy-node-border, var(--xy-node-border-default)); + border-radius: var( + --xy-node-border-radius, + var(--xy-node-border-radius-default) + ); } + .svelte-flow__node-input.selectable:hover, .svelte-flow__node-default.selectable:hover, .svelte-flow__node-output.selectable:hover, @@ -509,6 +569,7 @@ svg.svelte-flow__connectionline { var(--xy-node-boxshadow-hover-default) ); } + .svelte-flow__node-input.selectable.selected, .svelte-flow__node-input.selectable:focus, .svelte-flow__node-input.selectable:focus-visible, @@ -526,12 +587,14 @@ svg.svelte-flow__connectionline { var(--xy-node-boxshadow-selected-default) ); } + .svelte-flow__node-group { background-color: var( --xy-node-group-background-color, var(--xy-node-group-background-color-default) ); } + .svelte-flow__nodesselection-rect, .svelte-flow__selection { background: var( @@ -540,20 +603,15 @@ svg.svelte-flow__connectionline { ); border: var(--xy-selection-border, var(--xy-selection-border-default)); } + .svelte-flow__nodesselection-rect:focus, .svelte-flow__nodesselection-rect:focus-visible, .svelte-flow__selection:focus, .svelte-flow__selection:focus-visible { outline: none; } + .svelte-flow__controls-button:hover { - background: var( - --xy-controls-button-background-color-hover-props, - var( - --xy-controls-button-background-color-hover, - var(--xy-controls-button-background-color-hover-default) - ) - ); color: var( --xy-controls-button-color-hover-props, var( @@ -561,413 +619,490 @@ svg.svelte-flow__connectionline { var(--xy-controls-button-color-hover-default) ) ); + background: var( + --xy-controls-button-background-color-hover-props, + var( + --xy-controls-button-background-color-hover, + var(--xy-controls-button-background-color-hover-default) + ) + ); } + .svelte-flow__controls-button:disabled { pointer-events: none; } + .svelte-flow__controls-button:disabled svg { fill-opacity: 0.4; } + .svelte-flow__controls-button:last-child { border-bottom: none; } + .svelte-flow__resize-control { position: absolute; } + .svelte-flow__resize-control.left, .svelte-flow__resize-control.right { cursor: ew-resize; } + .svelte-flow__resize-control.top, .svelte-flow__resize-control.bottom { cursor: ns-resize; } + .svelte-flow__resize-control.top.left, .svelte-flow__resize-control.bottom.right { cursor: nwse-resize; } + .svelte-flow__resize-control.bottom.left, .svelte-flow__resize-control.top.right { cursor: nesw-resize; } + .svelte-flow__resize-control.handle { width: 4px; height: 4px; - border: 1px solid #fff; - border-radius: 1px; background-color: var( --xy-resize-background-color, var(--xy-resize-background-color-default) ); + border: 1px solid #fff; + border-radius: 1px; transform: translate(-50%, -50%); } + .svelte-flow__resize-control.handle.left { - left: 0; top: 50%; + left: 0; } + .svelte-flow__resize-control.handle.right { - left: 100%; top: 50%; + left: 100%; } + .svelte-flow__resize-control.handle.top { - left: 50%; top: 0; + left: 50%; } + .svelte-flow__resize-control.handle.bottom { - left: 50%; top: 100%; + left: 50%; } + .svelte-flow__resize-control.handle.top.left, .svelte-flow__resize-control.handle.bottom.left { left: 0; } + .svelte-flow__resize-control.handle.top.right, .svelte-flow__resize-control.handle.bottom.right { left: 100%; } + .svelte-flow__resize-control.line { border-color: var( --xy-resize-background-color, var(--xy-resize-background-color-default) ); - border-width: 0; border-style: solid; + border-width: 0; } + .svelte-flow__resize-control.line.left, .svelte-flow__resize-control.line.right { - width: 1px; - transform: translate(-50%); top: 0; + width: 1px; height: 100%; + transform: translate(-50%); } + .svelte-flow__resize-control.line.left { left: 0; border-left-width: 1px; } + .svelte-flow__resize-control.line.right { left: 100%; border-right-width: 1px; } + .svelte-flow__resize-control.line.top, .svelte-flow__resize-control.line.bottom { - height: 1px; - transform: translateY(-50%); left: 0; width: 100%; + height: 1px; + transform: translateY(-50%); } + .svelte-flow__resize-control.line.top { top: 0; border-top-width: 1px; } + .svelte-flow__resize-control.line.bottom { - border-bottom-width: 1px; top: 100%; + border-bottom-width: 1px; } + .svelte-flow__edge-label { - text-align: center; position: absolute; padding: 2px; font-size: 10px; - cursor: pointer; color: var(--xy-edge-label-color, var(--xy-edge-label-color-default)); + text-align: center; + cursor: pointer; background: var( --xy-edge-label-background-color, var(--xy-edge-label-background-color-default) ); } + .svelte-flow__nodes, .svelte-flow__edgelabel-renderer { z-index: 0; } + :root, :root .tf-theme-light { --tf-primary-color: #2563eb; --xy-node-boxshadow-selected: 0 0 0 1px var(--tf-primary-color); --xy-handle-background-color: var(--tf-primary-color); } + .tf-btn { display: flex; + gap: 2px; align-items: center; justify-content: center; - gap: 2px; + width: fit-content; + height: fit-content; + padding: 5px; + margin: 0; + cursor: pointer; background: #fff; border: 1px solid #ccc; - cursor: pointer; border-radius: 5px; - padding: 5px; - margin: 0; - height: fit-content; - width: fit-content; } + .tf-btn svg { - fill: currentColor; width: 16px; height: 16px; + fill: currentcolor; } + .tf-btn:hover { border: 1px solid var(--tf-primary-color); } + .tf-input, .tf-textarea { + box-sizing: border-box; display: flex; - border-radius: 5px; - border: 1px solid #ccc; padding: 5px 10px; - box-sizing: border-box; resize: vertical; outline: none; + border: 1px solid #ccc; + border-radius: 5px; } + .tf-input::placeholder, .tf-textarea::placeholder { color: #ccc; } + .tf-input:focus, .tf-textarea:focus { border-color: var(--tf-primary-color); box-shadow: 0 0 5px #51cbee33; } + .tf-input[disabled], .tf-textarea[disabled] { - background-color: #f0f0f0; - cursor: not-allowed; color: #aaa; + cursor: not-allowed; + background-color: #f0f0f0; } + .tf-select-input { display: flex; - border: 1px solid #ccc; + align-items: center; + justify-content: space-between; + height: 27px; padding: 3px 10px; - border-radius: 5px; font-size: 14px; - justify-content: space-between; - align-items: center; cursor: pointer; background: #fff; - height: 27px; + border: 1px solid #ccc; + border-radius: 5px; } + .tf-select-input:focus { border-color: var(--tf-primary-color); box-shadow: 0 0 5px #51cbee33; } + .tf-select-input-value { - height: 21px; - min-width: 10px; - font-size: 12px; display: flex; align-items: center; + min-width: 10px; + height: 21px; + font-size: 12px; } + .tf-select-input-arrow { display: block; width: 16px; height: 16px; color: #666; } + .tf-select-input-placeholder { color: #ccc; } + .tf-select-content { + z-index: 9999; + box-sizing: border-box; display: flex; flex-direction: column; - background: #fff; + width: max-content; + min-width: 100%; + padding: 5px; margin-top: 5px; + background: #fff; border: 1px solid #ccc; border-radius: 5px; - padding: 5px; - width: max-content; - min-width: 100%; - z-index: 9999; - box-sizing: border-box; } + .tf-select-content-item { display: flex; + gap: 2px; align-items: center; padding: 5px 10px; - border: none; + line-height: 100%; + cursor: pointer; background: #fff; + border: none; border-radius: 5px; - cursor: pointer; - line-height: 100%; - gap: 2px; } + .tf-select-content-item span { - width: 16px; display: flex; + width: 16px; } + .tf-select-content-item svg { width: 16px; height: 16px; margin: auto; } + .tf-select-content-item:hover { background: #f0f0f0; } + .tf-select-content-children { padding-left: 14px; } + .tf-checkbox { width: 14px; height: 14px; } + .tf-tabs { display: flex; + gap: 5px; align-items: center; justify-content: center; - gap: 5px; padding: 5px; - border-radius: 5px; - border: none; background: #f4f4f5; + border: none; + border-radius: 5px; } + .tf-tabs .tf-tabs-item { - flex-grow: 1; - padding: 5px 10px; - cursor: pointer; - border-radius: 5px; display: flex; + flex-grow: 1; align-items: center; justify-content: center; + padding: 5px 10px; font-size: 14px; color: #808088; + cursor: pointer; + border-radius: 5px; } + .tf-tabs .tf-tabs-item.active { - background: #fff; - color: #333; font-weight: 500; + color: #333; + background: #fff; box-shadow: 0 0 5px #00000026; } + h3.tf-heading { - font-weight: 700; - font-size: 14px; margin-top: 2px; margin-bottom: 3px; + font-size: 14px; + font-weight: 700; color: #333; } + .tf-collapse { border: none; border-radius: 5px; } + .tf-collapse-item-title { display: flex; align-items: center; - cursor: pointer; font-size: 14px; + cursor: pointer; } + .tf-collapse-item-title-icon { display: flex; + align-items: center; + justify-content: center; width: 26px; height: 26px; + padding: 3px; + margin-right: 10px; color: #2563eb; background: #cedafb; border-radius: 5px; - padding: 3px; - justify-content: center; - align-items: center; - margin-right: 10px; } + .tf-collapse-item-title-icon svg { width: 22px; height: 22px; color: #3474ff; } + .tf-collapse-item-title-arrow { display: block; width: 16px; height: 16px; margin-left: auto; } + .tf-collapse-item-description { - font-size: 12px; margin: 10px 0; + font-size: 12px; color: #999; } + .svelte-flow__nodes .svelte-flow__node { + box-sizing: border-box; border: 3px solid transparent; border-radius: 5px; - box-sizing: border-box; } + .svelte-flow__nodes .svelte-flow__node .svelte-flow__handle { + display: flex; + align-items: center; + justify-content: center; width: 16px; height: 16px; background: transparent; - display: flex; - justify-content: center; - align-items: center; border: none; } -.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle:after { - content: ' '; - background: #2563eb; + +.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle::after { width: 8px; height: 8px; + content: ' '; + background: #2563eb; border-radius: 100%; transition: width 0.1s, height 0.1s; } -.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle:hover:after { + +.svelte-flow__nodes .svelte-flow__node .svelte-flow__handle:hover::after { width: 16px; height: 16px; } -.svelte-flow__nodes .svelte-flow__node div.loop_handle_wrapper:after { - content: '循环体'; - background: #2563eb; + +.svelte-flow__nodes .svelte-flow__node div.loop_handle_wrapper::after { + display: flex; + align-items: center; + justify-content: center; width: 100px; height: 20px; - border-radius: 0; - display: flex; color: #fff; - justify-content: center; - align-items: center; + content: '循环体'; + background: #2563eb; + border-radius: 0; } -.svelte-flow__nodes .svelte-flow__node div.loop_handle_wrapper:hover:after { + +.svelte-flow__nodes .svelte-flow__node div.loop_handle_wrapper:hover::after { width: 100px; height: 20px; } -.svelte-flow__nodes .svelte-flow__node:after { - content: ' '; + +.svelte-flow__nodes .svelte-flow__node::after { position: absolute; - border-radius: 5px; top: -2px; left: -2px; - border: 1px solid #ccc; - height: calc(100% + 2px); width: calc(100% + 2px); + height: calc(100% + 2px); + content: ' '; + border: 1px solid #ccc; + border-radius: 5px; } + .svelte-flow__nodes .svelte-flow__node:hover { border: 3px solid #bacaef7d; } + .svelte-flow__nodes .svelte-flow__node.selectable.selected { border: 3px solid #bacaef7d; box-shadow: var(--xy-node-boxshadow-selected); } -.svelte-flow__nodes .svelte-flow__node:hover:after { + +.svelte-flow__nodes .svelte-flow__node:hover::after { display: none; } -.svelte-flow__nodes .svelte-flow__node.selectable.selected:after { + +.svelte-flow__nodes .svelte-flow__node.selectable.selected::after { display: none; } + .tf-node-wrapper { - border-radius: 5px; min-width: 300px; background: #fff; + border-radius: 5px; } + .tf-node-wrapper-title { - height: 30px; - background: #eff1f5; - color: #bcbcbc; - font-size: 12px; display: flex; align-items: center; + height: 30px; padding-left: 5px; - border-bottom: 1px solid #ccc; + font-size: 12px; font-weight: 300; + color: #bcbcbc; letter-spacing: 1px; + background: #eff1f5; + border-bottom: 1px solid #ccc; } + .tf-node-wrapper-body { padding: 10px; } + .svelte-flow__attribution a { display: none; } + .tf-toolbar { position: absolute; top: 10px; @@ -975,69 +1110,78 @@ h3.tf-heading { z-index: 9999; display: flex; gap: 5px; + transform: translate(-220px); transition: transform 0.5s ease, opacity 0.5s ease; - transform: translate(-220px); } + .tf-toolbar.show { transform: translate(0); } + .tf-toolbar-container { + width: 180px; + padding: 10px; background: #fff; border: 1px solid #eee; border-radius: 5px; box-shadow: 0 0 5px #0000001a; - padding: 10px; - width: 180px; } + .tf-toolbar-container-header { display: flex; } + .tf-toolbar-container-body { display: flex; margin-top: 20px; } + .tf-toolbar-container-body .tf-toolbar-container-base, .tf-toolbar-container-body .tf-toolbar-container-tools { display: flex; + flex-grow: 1; flex-direction: column; gap: 4px; - flex-grow: 1; } + .tf-toolbar-container-body .tf-toolbar-container-base .tf-btn, .tf-toolbar-container-body .tf-toolbar-container-tools .tf-btn { - border: none; - width: 100%; + gap: 10px; justify-content: flex-start; + width: 100%; height: 40px; - gap: 10px; cursor: grabbing; + border: none; border-radius: 5px; } + .tf-toolbar-container-body .tf-toolbar-container-base .tf-btn svg, .tf-toolbar-container-body .tf-toolbar-container-tools .tf-btn svg { width: 20px; height: 20px; fill: #2563eb; } + .tf-toolbar-container-body .tf-toolbar-container-base .tf-btn:hover, .tf-toolbar-container-body .tf-toolbar-container-tools .tf-btn:hover { background: #f1f1f1; } -.tinyflow-logo:after { - content: 'Tinyflow.ai'; - font-size: 145px; + +.tinyflow-logo::after { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; + font-size: 145px; font-weight: 800; color: #03153b54; text-shadow: 1px 3px 6px #cedafb, 0 0 0 #000, 1px 3px 6px #fff; + content: 'Tinyflow.ai'; opacity: 0.1; } diff --git a/apps/web-ele/src/components/summary-card/index.ts b/apps/web-ele/src/components/summary-card/index.ts new file mode 100644 index 000000000..598d73d61 --- /dev/null +++ b/apps/web-ele/src/components/summary-card/index.ts @@ -0,0 +1,2 @@ +export { default as SummaryCard } from './summary-card.vue'; +export type { SummaryCardProps } from './typing'; diff --git a/apps/web-ele/src/components/summary-card/summary-card.vue b/apps/web-ele/src/components/summary-card/summary-card.vue new file mode 100644 index 000000000..55fe7d17d --- /dev/null +++ b/apps/web-ele/src/components/summary-card/summary-card.vue @@ -0,0 +1,57 @@ + + + + + + + + + {{ title }} + + + + + + + + + + {{ Math.abs(Number(percent)) }}% + + + + + + diff --git a/apps/web-ele/src/components/summary-card/typing.ts b/apps/web-ele/src/components/summary-card/typing.ts new file mode 100644 index 000000000..0ef1fdc6f --- /dev/null +++ b/apps/web-ele/src/components/summary-card/typing.ts @@ -0,0 +1,11 @@ +export interface SummaryCardProps { + title: string; + tooltip?: string; + icon?: string; + iconColor?: string; + iconBgColor?: string; + prefix?: string; + value?: number; + decimals?: number; + percent?: number | string; +} diff --git a/apps/web-ele/src/views/mall/home/index.vue b/apps/web-ele/src/views/mall/home/index.vue new file mode 100644 index 000000000..21b6279b4 --- /dev/null +++ b/apps/web-ele/src/views/mall/home/index.vue @@ -0,0 +1,177 @@ + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/brand/data.ts b/apps/web-ele/src/views/mall/product/brand/data.ts new file mode 100644 index 000000000..654809ac9 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/brand/data.ts @@ -0,0 +1,132 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; +import { + CommonStatusEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '品牌名称', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'picUrl', + label: '品牌图片', + component: 'ImageUpload', + rules: 'required', + }, + { + fieldName: 'sort', + label: '品牌排序', + component: 'InputNumber', + componentProps: { + min: 0, + controlsPosition: 'right', + placeholder: '请输入品牌排序', + }, + rules: z.number().min(0).default(1), + }, + { + fieldName: 'status', + label: '品牌状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + { + fieldName: 'description', + label: '品牌描述', + component: 'Textarea', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '品牌名称', + component: 'Input', + }, + { + fieldName: 'status', + label: '品牌状态', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeGridPropTypes.Columns { + return [ + { + field: 'name', + title: '分类名称', + fixed: 'left', + }, + { + field: 'picUrl', + title: '品牌图片', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'sort', + title: '品牌排序', + }, + { + field: 'status', + title: '开启状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/product/brand/index.vue b/apps/web-ele/src/views/mall/product/brand/index.vue new file mode 100644 index 000000000..ab998994b --- /dev/null +++ b/apps/web-ele/src/views/mall/product/brand/index.vue @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/brand/modules/form.vue b/apps/web-ele/src/views/mall/product/brand/modules/form.vue new file mode 100644 index 000000000..dc46d4198 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/brand/modules/form.vue @@ -0,0 +1,83 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/category/data.ts b/apps/web-ele/src/views/mall/product/category/data.ts new file mode 100644 index 000000000..5efa27c02 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/category/data.ts @@ -0,0 +1,139 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { MallCategoryApi } from '#/api/mall/product/category'; + +import { handleTree } from '@vben/utils'; + +import { z } from '#/adapter/form'; +import { getCategoryList } from '#/api/mall/product/category'; +import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'parentId', + label: '上级分类', + component: 'ApiTreeSelect', + componentProps: { + allowClear: true, + api: async () => { + const data = await getCategoryList({ parentId: 0 }); + data.unshift({ + id: 0, + name: '顶级分类', + picUrl: '', + sort: 0, + status: 0, + }); + return handleTree(data); + }, + labelField: 'name', + valueField: 'id', + childrenField: 'children', + placeholder: '请选择上级分类', + treeDefaultExpandAll: true, + }, + rules: 'selectRequired', + }, + { + fieldName: 'name', + label: '分类名称', + component: 'Input', + componentProps: { + placeholder: '请输入分类名称', + }, + rules: 'required', + }, + { + fieldName: 'picUrl', + label: '移动端分类图', + component: 'ImageUpload', + rules: 'required', + }, + { + fieldName: 'sort', + label: '分类排序', + component: 'InputNumber', + componentProps: { + min: 0, + controlsPosition: 'right', + placeholder: '请输入分类排序', + }, + rules: z.number().min(0).default(1), + }, + { + fieldName: 'status', + label: '开启状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '分类名称', + component: 'Input', + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'name', + title: '分类名称', + align: 'left', + fixed: 'left', + treeNode: true, + }, + { + field: 'picUrl', + title: '移动端分类图', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'sort', + title: '分类排序', + }, + { + field: 'status', + title: '开启状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 300, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/product/category/index.vue b/apps/web-ele/src/views/mall/product/category/index.vue new file mode 100644 index 000000000..7a49ff79e --- /dev/null +++ b/apps/web-ele/src/views/mall/product/category/index.vue @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + {{ row.name }} + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/category/modules/form.vue b/apps/web-ele/src/views/mall/product/category/modules/form.vue new file mode 100644 index 000000000..44bb6ba3e --- /dev/null +++ b/apps/web-ele/src/views/mall/product/category/modules/form.vue @@ -0,0 +1,89 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/comment/data.ts b/apps/web-ele/src/views/mall/product/comment/data.ts new file mode 100644 index 000000000..fb4d87cfd --- /dev/null +++ b/apps/web-ele/src/views/mall/product/comment/data.ts @@ -0,0 +1,202 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; +import type { MallCommentApi } from '#/api/mall/product/comment'; + +import { getSpuSimpleList } from '#/api/mall/product/spu'; +import { getRangePickerDefaultProps } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'spuId', + label: '商品', + component: 'ApiSelect', + componentProps: { + api: getSpuSimpleList, + labelField: 'name', + valueField: 'id', + }, + rules: 'required', + }, + { + fieldName: 'userAvatar', + label: '用户头像', + component: 'ImageUpload', + rules: 'required', + }, + { + fieldName: 'userNickname', + label: '用户名称', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'content', + label: '评论内容', + component: 'Textarea', + rules: 'required', + }, + { + fieldName: 'descriptionScores', + label: '描述星级', + component: 'Rate', + rules: 'required', + }, + { + fieldName: 'benefitScores', + label: '服务星级', + component: 'Rate', + rules: 'required', + }, + { + fieldName: 'picUrls', + label: '评论图片', + component: 'ImageUpload', + componentProps: { + maxNumber: 9, + }, + rules: 'required', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'replyStatus', + label: '回复状态', + component: 'Select', + componentProps: { + options: [ + { label: '已回复', value: true }, + { label: '未回复', value: false }, + ], + }, + }, + { + fieldName: 'spuName', + label: '商品名称', + component: 'Input', + }, + { + fieldName: 'userNickname', + label: '用户名称', + component: 'Input', + }, + { + fieldName: 'orderId', + label: '订单编号', + component: 'Input', + }, + { + fieldName: 'createTime', + label: '评论时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns( + onStatusChange?: ( + newStatus: boolean, + row: T, + ) => PromiseLike, +): VxeGridPropTypes.Columns { + return [ + { + field: 'id', + title: '评论编号', + fixed: 'left', + }, + { + field: 'skuPicUrl', + title: '商品图片', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'spuName', + title: '商品名称', + minWidth: 200, + }, + { + field: 'skuProperties', + title: '商品属性', + minWidth: 200, + formatter: ({ cellValue }) => { + return cellValue && cellValue.length > 0 + ? cellValue + .map((item: any) => `${item.propertyName} : ${item.valueName}`) + .join('\n') + : '-'; + }, + }, + { + field: 'userNickname', + title: '用户名称', + }, + { + field: 'descriptionScores', + title: '商品评分', + }, + { + field: 'benefitScores', + title: '服务评分', + }, + { + field: 'content', + title: '评论内容', + }, + { + field: 'picUrls', + title: '评论图片', + cellRender: { + name: 'CellImages', + }, + }, + { + field: 'replyContent', + title: '回复内容', + }, + { + field: 'createTime', + title: '评论时间', + formatter: 'formatDateTime', + }, + { + field: 'visible', + title: '是否展示', + align: 'center', + cellRender: { + attrs: { beforeChange: onStatusChange }, + name: 'CellSwitch', + props: { + checkedValue: true, + unCheckedValue: false, + }, + }, + }, + { + title: '操作', + width: 80, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/product/comment/index.vue b/apps/web-ele/src/views/mall/product/comment/index.vue new file mode 100644 index 000000000..baddaa180 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/comment/index.vue @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/comment/modules/form.vue b/apps/web-ele/src/views/mall/product/comment/modules/form.vue new file mode 100644 index 000000000..1e8ed0fa3 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/comment/modules/form.vue @@ -0,0 +1,83 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/property/data.ts b/apps/web-ele/src/views/mall/product/property/data.ts new file mode 100644 index 000000000..733c8fdcb --- /dev/null +++ b/apps/web-ele/src/views/mall/product/property/data.ts @@ -0,0 +1,176 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { getPropertySimpleList } from '#/api/mall/product/property'; + +// ============================== 属性 ============================== + +/** 类型新增/修改的表单 */ +export function usePropertyFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '名称', + component: 'Input', + componentProps: { + placeholder: '请输入名称', + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + }, + }, + ]; +} + +/** 类型列表的搜索表单 */ +export function usePropertyGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '名称', + component: 'Input', + componentProps: { + placeholder: '请输入名称', + clearable: true, + }, + }, + ]; +} + +/** 类型列表的字段 */ +export function usePropertyGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + }, + { + field: 'name', + title: '名称', + }, + { + field: 'remark', + title: '备注', + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 160, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +// ============================== 值数据 ============================== + +/** 数据新增/修改的表单 */ +export function useValueFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'propertyId', + label: '属性编号', + component: 'ApiSelect', + componentProps: (values) => { + return { + api: getPropertySimpleList, + labelField: 'name', + valueField: 'id', + disabled: !!values.id, + }; + }, + rules: 'required', + dependencies: { + triggerFields: [''], + }, + }, + { + fieldName: 'name', + label: '名称', + component: 'Input', + componentProps: { + placeholder: '请输入名称', + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + }, + }, + ]; +} + +/** 字典数据列表搜索表单 */ +export function useValueGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '名称', + component: 'Input', + componentProps: { + clearable: true, + }, + }, + ]; +} + +/** + * 字典数据表格列 + */ +export function useValueGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + }, + { + field: 'name', + title: '属性值名称', + }, + { + field: 'remark', + title: '备注', + }, + { + title: '创建时间', + field: 'createTime', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 160, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/product/property/index.vue b/apps/web-ele/src/views/mall/product/property/index.vue new file mode 100644 index 000000000..97036c490 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/property/index.vue @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/property/modules/property-form.vue b/apps/web-ele/src/views/mall/product/property/modules/property-form.vue new file mode 100644 index 000000000..b37ac6de6 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/property/modules/property-form.vue @@ -0,0 +1,88 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/property/modules/property-grid.vue b/apps/web-ele/src/views/mall/product/property/modules/property-grid.vue new file mode 100644 index 000000000..9bc0f00a7 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/property/modules/property-grid.vue @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/property/modules/value-form.vue b/apps/web-ele/src/views/mall/product/property/modules/value-form.vue new file mode 100644 index 000000000..b66085663 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/property/modules/value-form.vue @@ -0,0 +1,100 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/property/modules/value-grid.vue b/apps/web-ele/src/views/mall/product/property/modules/value-grid.vue new file mode 100644 index 000000000..3c58b1ca1 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/property/modules/value-grid.vue @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/data.ts b/apps/web-ele/src/views/mall/product/spu/data.ts new file mode 100644 index 000000000..6ceb265ea --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/data.ts @@ -0,0 +1,117 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { MallSpuApi } from '#/api/mall/product/spu'; + +import { handleTree } from '@vben/utils'; + +import { getCategoryList } from '#/api/mall/product/category'; +import { getRangePickerDefaultProps } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '商品名称', + component: 'Input', + }, + { + fieldName: 'categoryId', + label: '商品分类', + component: 'ApiTreeSelect', + componentProps: { + api: async () => { + const res = await getCategoryList({}); + return handleTree(res, 'id', 'parentId', 'children'); + }, + fieldNames: { label: 'name', value: 'id', children: 'children' }, + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns( + onStatusChange?: ( + newStatus: number, + row: T, + ) => PromiseLike, +): VxeTableGridOptions['columns'] { + return [ + { + type: 'expand', + width: 80, + slots: { content: 'expand_content' }, + fixed: 'left', + }, + { + field: 'id', + title: '商品编号', + fixed: 'left', + }, + { + field: 'name', + title: '商品名称', + fixed: 'left', + minWidth: 200, + }, + { + field: 'picUrl', + title: '商品图片', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'price', + title: '价格', + formatter: 'formatAmount2', + }, + { + field: 'salesCount', + title: '销量', + }, + { + field: 'stock', + title: '库存', + }, + { + field: 'sort', + title: '排序', + }, + { + field: 'status', + title: '销售状态', + cellRender: { + attrs: { beforeChange: onStatusChange }, + name: 'CellSwitch', + props: { + checkedValue: 1, + checkedChildren: '上架', + unCheckedValue: 0, + unCheckedChildren: '下架', + }, + }, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 300, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/product/spu/index.vue b/apps/web-ele/src/views/mall/product/spu/index.vue new file mode 100644 index 000000000..97e336c5a --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/index.vue @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + {{ treeToString(categoryList, row.categoryId) }} + + + {{ row.name }} + + + + {{ fenToYuan(row.marketPrice) }} 元 + + + {{ fenToYuan(row.costPrice) }} 元 + + + {{ row.browseCount }} + + + {{ row.virtualSalesCount }} + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/modules/detail.vue b/apps/web-ele/src/views/mall/product/spu/modules/detail.vue new file mode 100644 index 000000000..c28ebc684 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/modules/detail.vue @@ -0,0 +1,3 @@ + + +detail diff --git a/apps/web-ele/src/views/mall/product/spu/modules/form.vue b/apps/web-ele/src/views/mall/product/spu/modules/form.vue new file mode 100644 index 000000000..5b6413575 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/modules/form.vue @@ -0,0 +1,3 @@ + + +form diff --git a/apps/web-ele/src/views/mall/promotion/article/category/data.ts b/apps/web-ele/src/views/mall/promotion/article/category/data.ts new file mode 100644 index 000000000..d03a5d879 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/article/category/data.ts @@ -0,0 +1,135 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '分类名称', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'picUrl', + label: '图标地址', + component: 'ImageUpload', + }, + { + fieldName: 'sort', + label: '排序', + component: 'InputNumber', + componentProps: { + min: 0, + controlsPosition: 'right', + placeholder: '请输入排序', + }, + rules: 'required', + }, + { + fieldName: 'status', + label: '状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: 'required', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '分类名称', + component: 'Input', + componentProps: { + placeholder: '请输入分类名称', + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + placeholder: '请选择状态', + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeGridPropTypes.Columns { + return [ + { + title: '编号', + field: 'id', + width: 100, + }, + { + title: '分类名称', + field: 'name', + minWidth: 240, + }, + { + title: '分类图片', + field: 'picUrl', + width: 80, + cellRender: { + name: 'CellImage', + }, + }, + { + title: '状态', + field: 'status', + width: 150, + cellRender: { + name: 'CellDictTag', + props: { + dictType: DICT_TYPE.COMMON_STATUS, + }, + }, + }, + { + title: '排序', + field: 'sort', + width: 150, + }, + { + title: '创建时间', + field: 'createTime', + width: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/article/category/index.vue b/apps/web-ele/src/views/mall/promotion/article/category/index.vue new file mode 100644 index 000000000..c1ffe57f8 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/article/category/index.vue @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/article/category/modules/form.vue b/apps/web-ele/src/views/mall/promotion/article/category/modules/form.vue new file mode 100644 index 000000000..45cc3e373 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/article/category/modules/form.vue @@ -0,0 +1,90 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/article/data.ts b/apps/web-ele/src/views/mall/promotion/article/data.ts new file mode 100644 index 000000000..5b03258fb --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/article/data.ts @@ -0,0 +1,210 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; +import { getSimpleArticleCategoryList } from '#/api/mall/promotion/articleCategory'; +import { + CommonStatusEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'title', + label: '文章标题', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'categoryId', + label: '文章分类', + component: 'ApiSelect', + componentProps: { + api: getSimpleArticleCategoryList, + labelField: 'name', + valueField: 'id', + }, + rules: 'required', + }, + { + fieldName: 'author', + label: '文章作者', + component: 'Input', + }, + { + fieldName: 'introduction', + label: '文章简介', + component: 'Input', + }, + { + fieldName: 'picUrl', + label: '文章封面', + component: 'ImageUpload', + rules: 'required', + }, + { + fieldName: 'recommendHot', + label: '是否热门', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + fieldName: 'recommendBanner', + label: '是否轮播图', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), + buttonStyle: 'solid', + optionType: 'button', + }, + }, + { + // TODO: 商品关联 + fieldName: 'spuId', + label: '商品关联', + component: 'Input', + }, + { + fieldName: 'sort', + label: '排序', + component: 'InputNumber', + componentProps: { + min: 0, + controlsPosition: 'right', + placeholder: '请输入品牌排序', + }, + rules: z.number().min(0).default(1), + }, + { + fieldName: 'status', + label: '状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + { + fieldName: 'description', + label: '文章内容', + component: 'RichTextarea', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '文章分类', + component: 'ApiSelect', + componentProps: { + api: getSimpleArticleCategoryList, + labelField: 'name', + valueField: 'id', + }, + }, + { + fieldName: 'title', + label: '文章标题', + component: 'Input', + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeGridPropTypes.Columns { + return [ + { + field: 'id', + title: '编号', + fixed: 'left', + }, + { + field: 'title', + title: '标题', + }, + { + field: 'picUrl', + title: '封面', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'categoryId', + title: '分类', + }, + { + field: 'browseCount', + title: '浏览量', + }, + { + field: 'author', + title: '作者', + }, + { + field: 'introduction', + title: '文章简介', + }, + { + field: 'sort', + title: '排序', + }, + { + field: 'status', + title: '状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/article/index.vue b/apps/web-ele/src/views/mall/promotion/article/index.vue new file mode 100644 index 000000000..39e1ae905 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/article/index.vue @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/article/modules/form.vue b/apps/web-ele/src/views/mall/promotion/article/modules/form.vue new file mode 100644 index 000000000..fc1d3333c --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/article/modules/form.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/banner/data.ts b/apps/web-ele/src/views/mall/promotion/banner/data.ts new file mode 100644 index 000000000..cdc7acbe0 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/banner/data.ts @@ -0,0 +1,175 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'title', + label: 'Banner标题', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'picUrl', + label: '图片地址', + component: 'ImageUpload', + rules: 'required', + }, + { + fieldName: 'position', + label: '定位', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.PROMOTION_BANNER_POSITION, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: 'required', + }, + { + fieldName: 'url', + label: '跳转地址', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'sort', + label: '排序', + component: 'InputNumber', + componentProps: { + min: 0, + controlsPosition: 'right', + placeholder: '请输入排序', + }, + rules: 'required', + }, + { + fieldName: 'status', + label: '状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: 'required', + }, + { + fieldName: 'memo', + label: '描述', + component: 'Textarea', + componentProps: { + rows: 4, + placeholder: '请输入描述', + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'title', + label: 'Banner标题', + component: 'Input', + componentProps: { + placeholder: '请输入Banner标题', + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + placeholder: '请选择状态', + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeGridPropTypes.Columns { + return [ + { + title: 'Banner标题', + field: 'title', + }, + { + title: '图片', + field: 'picUrl', + width: 80, + cellRender: { + name: 'CellImage', + }, + }, + { + title: '状态', + field: 'status', + width: 150, + cellRender: { + name: 'CellDictTag', + props: { + dictType: DICT_TYPE.COMMON_STATUS, + }, + }, + }, + { + title: '定位', + field: 'position', + width: 150, + cellRender: { + name: 'CellDictTag', + props: { + dictType: DICT_TYPE.PROMOTION_BANNER_POSITION, + }, + }, + }, + { + title: '跳转地址', + field: 'url', + }, + { + title: '创建时间', + field: 'createTime', + width: 180, + formatter: 'formatDateTime', + }, + { + title: '排序', + field: 'sort', + width: 100, + }, + { + title: '描述', + field: 'memo', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/banner/index.vue b/apps/web-ele/src/views/mall/promotion/banner/index.vue new file mode 100644 index 000000000..a5fb2050e --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/banner/index.vue @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/banner/modules/form.vue b/apps/web-ele/src/views/mall/promotion/banner/modules/form.vue new file mode 100644 index 000000000..cc04080c5 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/banner/modules/form.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/bargain/activity/data.ts b/apps/web-ele/src/views/mall/promotion/bargain/activity/data.ts new file mode 100644 index 000000000..80285f23c --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/bargain/activity/data.ts @@ -0,0 +1,262 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { formatDate } from '@vben/utils'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + }, + rules: 'required', + }, + { + fieldName: 'startTime', + label: '开始时间', + component: 'DatePicker', + componentProps: { + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + placeholder: '请选择开始时间', + }, + rules: 'required', + }, + { + fieldName: 'endTime', + label: '结束时间', + component: 'DatePicker', + componentProps: { + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + placeholder: '请选择结束时间', + }, + rules: 'required', + }, + { + fieldName: 'bargainFirstPrice', + label: '砍价起始价格(元)', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + step: 0.01, + placeholder: '请输入砍价起始价格', + }, + rules: 'required', + }, + { + fieldName: 'bargainMinPrice', + label: '砍价底价(元)', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + step: 0.01, + placeholder: '请输入砍价底价', + }, + rules: 'required', + }, + { + fieldName: 'stock', + label: '活动库存', + component: 'InputNumber', + componentProps: { + min: 1, + placeholder: '请输入活动库存', + }, + rules: 'required', + }, + { + fieldName: 'helpMaxCount', + label: '助力人数', + component: 'InputNumber', + componentProps: { + min: 1, + placeholder: '请输入助力人数', + }, + rules: 'required', + }, + { + fieldName: 'bargainCount', + label: '砍价次数', + component: 'InputNumber', + componentProps: { + min: 1, + placeholder: '请输入砍价次数', + }, + rules: 'required', + }, + { + fieldName: 'totalLimitCount', + label: '购买限制', + component: 'InputNumber', + componentProps: { + min: 1, + placeholder: '请输入购买限制', + }, + rules: 'required', + }, + { + fieldName: 'randomMinPrice', + label: '最小砍价金额(元)', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + step: 0.01, + placeholder: '请输入最小砍价金额', + }, + }, + { + fieldName: 'randomMaxPrice', + label: '最大砍价金额(元)', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + step: 0.01, + placeholder: '请输入最大砍价金额', + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + clearable: true, + }, + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '活动编号', + minWidth: 80, + }, + { + field: 'name', + title: '活动名称', + minWidth: 140, + }, + { + field: 'activityTime', + title: '活动时间', + minWidth: 210, + formatter: ({ row }) => { + if (!row.startTime || !row.endTime) return ''; + return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`; + }, + }, + { + field: 'picUrl', + title: '商品图片', + minWidth: 80, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + }, + }, + }, + { + field: 'spuName', + title: '商品标题', + minWidth: 300, + }, + { + field: 'bargainFirstPrice', + title: '起始价格', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'bargainMinPrice', + title: '砍价底价', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'recordUserCount', + title: '总砍价人数', + minWidth: 100, + }, + { + field: 'recordSuccessUserCount', + title: '成功砍价人数', + minWidth: 110, + }, + { + field: 'helpUserCount', + title: '助力人数', + minWidth: 100, + }, + { + field: 'status', + title: '活动状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'stock', + title: '库存', + minWidth: 80, + }, + { + field: 'totalStock', + title: '总库存', + minWidth: 80, + }, + { + field: 'createTime', + title: '创建时间', + width: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 150, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/bargain/activity/index.vue b/apps/web-ele/src/views/mall/promotion/bargain/activity/index.vue new file mode 100644 index 000000000..68100d3c7 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/bargain/activity/index.vue @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/bargain/activity/modules/form.vue b/apps/web-ele/src/views/mall/promotion/bargain/activity/modules/form.vue new file mode 100644 index 000000000..aa82b5132 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/bargain/activity/modules/form.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/bargain/record/data.ts b/apps/web-ele/src/views/mall/promotion/bargain/record/data.ts new file mode 100644 index 000000000..9bd74a66e --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/bargain/record/data.ts @@ -0,0 +1,161 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'status', + label: '砍价状态', + component: 'Select', + componentProps: { + placeholder: '请选择砍价状态', + clearable: true, + options: getDictOptions( + DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS, + 'number', + ), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + clearable: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + minWidth: 50, + }, + { + field: 'avatar', + title: '用户头像', + minWidth: 120, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + shape: 'circle', + }, + }, + }, + { + field: 'nickname', + title: '用户昵称', + minWidth: 100, + }, + { + field: 'createTime', + title: '发起时间', + width: 180, + formatter: 'formatDateTime', + }, + { + field: 'activity.name', + title: '砍价活动', + minWidth: 150, + }, + { + field: 'activity.bargainMinPrice', + title: '最低价', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'bargainPrice', + title: '当前价', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'activity.helpMaxCount', + title: '总砍价次数', + minWidth: 100, + }, + { + field: 'helpCount', + title: '剩余砍价次数', + minWidth: 100, + }, + { + field: 'status', + title: '砍价状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS }, + }, + }, + { + field: 'endTime', + title: '结束时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'orderId', + title: '订单编号', + minWidth: 100, + }, + { + title: '操作', + width: 100, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 助力列表表格列配置 */ +export function useHelpGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'userId', + title: '用户编号', + minWidth: 80, + }, + { + field: 'avatar', + title: '用户头像', + minWidth: 80, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + shape: 'circle', + }, + }, + }, + { + field: 'nickname', + title: '用户昵称', + minWidth: 100, + }, + { + field: 'reducePrice', + title: '砍价金额', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'createTime', + title: '助力时间', + width: 180, + formatter: 'formatDateTime', + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/bargain/record/index.vue b/apps/web-ele/src/views/mall/promotion/bargain/record/index.vue new file mode 100644 index 000000000..b9f6fb3ac --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/bargain/record/index.vue @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/bargain/record/modules/list.vue b/apps/web-ele/src/views/mall/promotion/bargain/record/modules/list.vue new file mode 100644 index 000000000..4d584bdc4 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/bargain/record/modules/list.vue @@ -0,0 +1,67 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/combination/activity/data.ts b/apps/web-ele/src/views/mall/promotion/combination/activity/data.ts new file mode 100644 index 000000000..e08dfa662 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/combination/activity/data.ts @@ -0,0 +1,238 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { formatDate } from '@vben/utils'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 表单配置 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + }, + rules: 'required', + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + rules: 'required', + }, + { + fieldName: 'startTime', + label: '开始时间', + component: 'DatePicker', + componentProps: { + placeholder: '请选择开始时间', + showTime: false, + valueFormat: 'x', + format: 'YYYY-MM-DD', + }, + rules: 'required', + }, + { + fieldName: 'endTime', + label: '结束时间', + component: 'DatePicker', + componentProps: { + placeholder: '请选择结束时间', + showTime: false, + valueFormat: 'x', + format: 'YYYY-MM-DD', + }, + rules: 'required', + }, + { + fieldName: 'userSize', + label: '用户数量', + component: 'InputNumber', + componentProps: { + placeholder: '请输入用户数量', + min: 2, + }, + rules: 'required', + }, + { + fieldName: 'limitDuration', + label: '限制时长', + component: 'InputNumber', + componentProps: { + placeholder: '请输入限制时长(小时)', + min: 0, + }, + rules: 'required', + }, + { + fieldName: 'totalLimitCount', + label: '总限购数量', + component: 'InputNumber', + componentProps: { + placeholder: '请输入总限购数量', + min: 0, + }, + }, + { + fieldName: 'singleLimitCount', + label: '单次限购数量', + component: 'InputNumber', + componentProps: { + placeholder: '请输入单次限购数量', + min: 0, + }, + }, + { + fieldName: 'virtualGroup', + label: '虚拟成团', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'), + }, + }, + { + // TODO + fieldName: 'spuId', + label: '拼团商品', + component: 'Input', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + clearable: true, + }, + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '活动编号', + minWidth: 80, + }, + { + field: 'name', + title: '活动名称', + minWidth: 140, + }, + { + field: 'activityTime', + title: '活动时间', + minWidth: 210, + formatter: ({ row }) => { + if (!row.startTime || !row.endTime) return ''; + return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`; + }, + }, + { + field: 'picUrl', + title: '商品图片', + minWidth: 80, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + }, + }, + }, + { + field: 'spuName', + title: '商品标题', + minWidth: 300, + }, + { + field: 'marketPrice', + title: '原价', + minWidth: 100, + formatter: ({ cellValue }) => { + return `¥${(cellValue / 100).toFixed(2)}`; + }, + }, + { + field: 'combinationPrice', + title: '拼团价', + minWidth: 100, + formatter: ({ row }) => { + if (!row.products || row.products.length === 0) return ''; + const combinationPrice = Math.min( + ...row.products.map((item: any) => item.combinationPrice), + ); + return `¥${(combinationPrice / 100).toFixed(2)}`; + }, + }, + { + field: 'groupCount', + title: '开团组数', + minWidth: 100, + }, + { + field: 'groupSuccessCount', + title: '成团组数', + minWidth: 100, + }, + { + field: 'recordCount', + title: '购买次数', + minWidth: 100, + }, + { + field: 'status', + title: '活动状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 200, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/combination/activity/index.vue b/apps/web-ele/src/views/mall/promotion/combination/activity/index.vue new file mode 100644 index 000000000..deb7af8a1 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/combination/activity/index.vue @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/combination/activity/modules/form.vue b/apps/web-ele/src/views/mall/promotion/combination/activity/modules/form.vue new file mode 100644 index 000000000..3d02d550c --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/combination/activity/modules/form.vue @@ -0,0 +1,93 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/combination/record/data.ts b/apps/web-ele/src/views/mall/promotion/combination/record/data.ts new file mode 100644 index 000000000..3a5082170 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/combination/record/data.ts @@ -0,0 +1,177 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'status', + label: '拼团状态', + component: 'Select', + componentProps: { + placeholder: '请选择拼团状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + placeholder: ['开始时间', '结束时间'], + clearable: true, + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '拼团编号', + minWidth: 80, + }, + { + field: 'avatar', + title: '头像', + minWidth: 80, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + shape: 'circle', + }, + }, + }, + { + field: 'nickname', + title: '昵称', + minWidth: 100, + }, + { + field: 'headId', + title: '开团团长', + minWidth: 100, + }, + { + field: 'picUrl', + title: '拼团商品图', + minWidth: 80, + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'spuName', + title: '拼团商品', + minWidth: 120, + }, + { + field: 'activityName', + title: '拼团活动', + minWidth: 140, + }, + { + field: 'userSize', + title: '几人团', + minWidth: 80, + }, + { + field: 'userCount', + title: '参与人数', + minWidth: 80, + }, + { + field: 'createTime', + title: '参团时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'endTime', + title: '结束时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'status', + title: '拼团状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + title: '操作', + width: 100, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 用户列表表格列配置 */ +export function useUserGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + minWidth: 80, + }, + { + field: 'avatar', + title: '用户头像', + minWidth: 80, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + shape: 'circle', + }, + }, + }, + { + field: 'nickname', + title: '用户昵称', + minWidth: 100, + }, + { + field: 'headId', + title: '开团团长', + minWidth: 100, + formatter: ({ cellValue }) => { + return cellValue === 0 ? '团长' : '团员'; + }, + }, + { + field: 'createTime', + title: '参团时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'endTime', + title: '结束时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'status', + title: '拼团状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/combination/record/index.vue b/apps/web-ele/src/views/mall/promotion/combination/record/index.vue new file mode 100644 index 000000000..ebcb71103 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/combination/record/index.vue @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/combination/record/modules/list.vue b/apps/web-ele/src/views/mall/promotion/combination/record/modules/list.vue new file mode 100644 index 000000000..459cd34d3 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/combination/record/modules/list.vue @@ -0,0 +1,63 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/coupon/data.ts b/apps/web-ele/src/views/mall/promotion/coupon/data.ts new file mode 100644 index 000000000..36c9fe714 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/coupon/data.ts @@ -0,0 +1,129 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +import { discountFormat } from './formatter'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'nickname', + label: '会员昵称', + component: 'Input', + componentProps: { + placeholder: '请输入会员昵称', + clearable: true, + }, + }, + { + fieldName: 'createTime', + label: '领取时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + clearable: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'nickname', + title: '会员昵称', + minWidth: 100, + }, + { + field: 'name', + title: '优惠券名称', + minWidth: 140, + }, + { + field: 'productScope', + title: '类型', + minWidth: 110, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE }, + }, + }, + { + field: 'discountType', + title: '优惠', + minWidth: 110, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE }, + }, + }, + { + field: 'discountPrice', + title: '优惠力度', + minWidth: 110, + formatter: ({ row }) => { + return discountFormat(row); + }, + }, + { + field: 'takeType', + title: '领取方式', + minWidth: 110, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE }, + }, + }, + { + field: 'status', + title: '状态', + minWidth: 110, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_COUPON_STATUS }, + }, + }, + { + field: 'createTime', + title: '领取时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'useTime', + title: '使用时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 100, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} + +/** 获取状态选项卡配置 */ +export function getStatusTabs() { + const tabs = [ + { + label: '全部', + value: 'all', + }, + ]; + + // 添加字典状态选项 + const statusOptions = getDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS); + for (const option of statusOptions) { + tabs.push({ + label: option.label, + value: String(option.value), + }); + } + + return tabs; +} diff --git a/apps/web-ele/src/views/mall/promotion/coupon/formatter.ts b/apps/web-ele/src/views/mall/promotion/coupon/formatter.ts new file mode 100644 index 000000000..41e6ec377 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/coupon/formatter.ts @@ -0,0 +1,65 @@ +import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate'; + +import { floatToFixed2, formatDate } from '@vben/utils'; + +import { + CouponTemplateValidityTypeEnum, + PromotionDiscountTypeEnum, +} from '#/utils'; + +// 格式化【优惠金额/折扣】 +export function discountFormat(row: MallCouponTemplateApi.CouponTemplate) { + if (row.discountType === PromotionDiscountTypeEnum.PRICE.type) { + return `¥${floatToFixed2(row.discountPrice)}`; + } + if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) { + return `${row.discountPercent}%`; + } + return `未知【${row.discountType}】`; +} + +// 格式化【领取上限】 +export function takeLimitCountFormat( + row: MallCouponTemplateApi.CouponTemplate, +) { + if (row.takeLimitCount) { + if (row.takeLimitCount === -1) { + return '无领取限制'; + } + return `${row.takeLimitCount} 张/人`; + } else { + return ' '; + } +} + +// 格式化【有效期限】 +export function validityTypeFormat(row: MallCouponTemplateApi.CouponTemplate) { + if (row.validityType === CouponTemplateValidityTypeEnum.DATE.type) { + return `${formatDate(row.validStartTime)} 至 ${formatDate(row.validEndTime)}`; + } + if (row.validityType === CouponTemplateValidityTypeEnum.TERM.type) { + return `领取后第 ${row.fixedStartTerm} - ${row.fixedEndTerm} 天内可用`; + } + return `未知【${row.validityType}】`; +} + +// 格式化【totalCount】 +export function totalCountFormat(row: MallCouponTemplateApi.CouponTemplate) { + if (row.totalCount === -1) { + return '不限制'; + } + return row.totalCount; +} + +// 格式化【剩余数量】 +export function remainedCountFormat(row: MallCouponTemplateApi.CouponTemplate) { + if (row.totalCount === -1) { + return '不限制'; + } + return row.totalCount - row.takeCount; +} + +// 格式化【最低消费】 +export function usePriceFormat(row: MallCouponTemplateApi.CouponTemplate) { + return `¥${floatToFixed2(row.usePrice)}`; +} diff --git a/apps/web-ele/src/views/mall/promotion/coupon/index.vue b/apps/web-ele/src/views/mall/promotion/coupon/index.vue new file mode 100644 index 000000000..b49e3e75c --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/coupon/index.vue @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/coupon/template/data.ts b/apps/web-ele/src/views/mall/promotion/coupon/template/data.ts new file mode 100644 index 000000000..e2359f2d4 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/coupon/template/data.ts @@ -0,0 +1,252 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +// 格式化函数移到组件内部实现 +import { z } from '#/adapter/form'; +import { + CommonStatusEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +import { + discountFormat, + remainedCountFormat, + takeLimitCountFormat, + totalCountFormat, + validityTypeFormat, +} from '../formatter'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '优惠券名称', + component: 'Input', + componentProps: { + placeholder: '请输入优惠券名称', + }, + rules: 'required', + }, + { + fieldName: 'description', + label: '优惠券描述', + component: 'Textarea', + }, + // TODO + { + fieldName: 'productScope', + label: '优惠类型', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'), + }, + rules: 'required', + }, + { + fieldName: 'takeType', + label: '领取方式', + component: 'Select', + componentProps: { + placeholder: '请选择领取方式', + options: getDictOptions(DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE, 'number'), + }, + rules: 'required', + }, + { + fieldName: 'validityType', + label: '有效期类型', + component: 'Select', + componentProps: { + placeholder: '请选择有效期类型', + options: getDictOptions( + DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE, + 'number', + ), + }, + rules: 'required', + }, + { + fieldName: 'totalCount', + label: '发放数量', + component: 'InputNumber', + componentProps: { + min: 0, + placeholder: '请输入发放数量', + }, + rules: 'required', + }, + { + fieldName: 'takeLimitCount', + label: '领取上限', + component: 'InputNumber', + componentProps: { + min: 0, + placeholder: '请输入领取上限', + }, + rules: 'required', + }, + { + fieldName: 'status', + label: '优惠券状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '优惠券名称', + component: 'Input', + componentProps: { + placeholder: '请输入优惠券名称', + clearable: true, + }, + }, + { + fieldName: 'discountType', + label: '优惠类型', + component: 'Select', + componentProps: { + placeholder: '请选择优惠类型', + clearable: true, + options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'), + }, + }, + { + fieldName: 'status', + label: '优惠券状态', + component: 'Select', + componentProps: { + placeholder: '请选择优惠券状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + clearable: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: 'checkbox', width: 40 }, + { + field: 'name', + title: '优惠券名称', + minWidth: 140, + }, + { + field: 'productScope', + title: '类型', + minWidth: 130, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE }, + }, + }, + { + field: 'discountType', + title: '优惠', + minWidth: 110, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_DISCOUNT_TYPE }, + }, + }, + { + field: 'discountPrice', + title: '优惠力度', + minWidth: 110, + formatter: ({ row }) => { + return discountFormat(row); + }, + }, + { + field: 'takeType', + title: '领取方式', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE }, + }, + }, + { + field: 'validityType', + title: '使用时间', + minWidth: 180, + formatter: ({ row }) => { + return validityTypeFormat(row); + }, + }, + { + field: 'totalCount', + title: '发放数量', + minWidth: 100, + formatter: ({ row }) => { + return totalCountFormat(row); + }, + }, + { + field: 'remainedCount', + title: '剩余数量', + minWidth: 100, + formatter: ({ row }) => { + return remainedCountFormat(row); + }, + }, + { + field: 'takeLimitCount', + title: '领取上限', + minWidth: 100, + formatter: ({ row }) => { + return takeLimitCountFormat(row); + }, + }, + { + field: 'status', + title: '状态', + minWidth: 100, + slots: { default: 'status' }, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 120, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/coupon/template/index.vue b/apps/web-ele/src/views/mall/promotion/coupon/template/index.vue new file mode 100644 index 000000000..cde8a304b --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/coupon/template/index.vue @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/coupon/template/modules/form.vue b/apps/web-ele/src/views/mall/promotion/coupon/template/modules/form.vue new file mode 100644 index 000000000..12caa3be1 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/coupon/template/modules/form.vue @@ -0,0 +1,89 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/discountActivity/data.ts b/apps/web-ele/src/views/mall/promotion/discountActivity/data.ts new file mode 100644 index 000000000..dffafe32d --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/discountActivity/data.ts @@ -0,0 +1,159 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { formatDate } from '@vben/utils'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 表单配置 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + }, + rules: 'required', + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + rules: 'required', + }, + { + fieldName: 'startTime', + label: '开始时间', + component: 'DatePicker', + componentProps: { + placeholder: '请选择开始时间', + showTime: false, + valueFormat: 'x', + format: 'YYYY-MM-DD', + }, + rules: 'required', + }, + { + fieldName: 'endTime', + label: '结束时间', + component: 'DatePicker', + componentProps: { + placeholder: '请选择结束时间', + showTime: false, + valueFormat: 'x', + format: 'YYYY-MM-DD', + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 4, + }, + }, + // TODO + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + clearable: true, + }, + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'activeTime', + label: '活动时间', + component: 'RangePicker', + componentProps: { + placeholder: ['开始时间', '结束时间'], + clearable: true, + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '活动编号', + minWidth: 80, + }, + { + field: 'name', + title: '活动名称', + minWidth: 140, + }, + { + field: 'activityTime', + title: '活动时间', + minWidth: 210, + formatter: ({ row }) => { + if (!row.startTime || !row.endTime) return ''; + return `${formatDate(row.startTime, 'YYYY-MM-DD')} ~ ${formatDate(row.endTime, 'YYYY-MM-DD')}`; + }, + }, + { + field: 'status', + title: '活动状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'remark', + title: '备注', + minWidth: 200, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 150, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/discountActivity/index.vue b/apps/web-ele/src/views/mall/promotion/discountActivity/index.vue new file mode 100644 index 000000000..c99e387d5 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/discountActivity/index.vue @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/discountActivity/modules/form.vue b/apps/web-ele/src/views/mall/promotion/discountActivity/modules/form.vue new file mode 100644 index 000000000..cc9c46bb0 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/discountActivity/modules/form.vue @@ -0,0 +1,98 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/diy/page/data.ts b/apps/web-ele/src/views/mall/promotion/diy/page/data.ts new file mode 100644 index 000000000..554f6829d --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/diy/page/data.ts @@ -0,0 +1,109 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +/** 表单配置 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '页面名称', + component: 'Input', + componentProps: { + placeholder: '请输入页面名称', + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 4, + }, + }, + { + fieldName: 'previewPicUrls', + component: 'ImageUpload', + label: '预览图', + componentProps: { + maxNumber: 10, + multiple: true, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '页面名称', + component: 'Input', + componentProps: { + placeholder: '请输入页面名称', + clearable: true, + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + placeholder: ['开始时间', '结束时间'], + clearable: true, + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + minWidth: 80, + }, + { + field: 'previewPicUrls', + title: '预览图', + minWidth: 120, + cellRender: { + name: 'CellImages', + }, + }, + { + field: 'name', + title: '页面名称', + minWidth: 150, + }, + { + field: 'remark', + title: '备注', + minWidth: 200, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 200, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/diy/page/index.vue b/apps/web-ele/src/views/mall/promotion/diy/page/index.vue new file mode 100644 index 000000000..e5a60f46d --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/diy/page/index.vue @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/diy/page/modules/form.vue b/apps/web-ele/src/views/mall/promotion/diy/page/modules/form.vue new file mode 100644 index 000000000..f2b9fd24f --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/diy/page/modules/form.vue @@ -0,0 +1,92 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/diy/template/data.ts b/apps/web-ele/src/views/mall/promotion/diy/template/data.ts new file mode 100644 index 000000000..0c46faf7e --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/diy/template/data.ts @@ -0,0 +1,120 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE } from '#/utils/dict'; + +/** 表单配置 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '模板名称', + component: 'Input', + componentProps: { + placeholder: '请输入模板名称', + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 4, + }, + }, + { + fieldName: 'previewPicUrls', + component: 'ImageUpload', + label: '预览图', + componentProps: { + maxNumber: 10, + multiple: true, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '模板名称', + component: 'Input', + componentProps: { + placeholder: '请输入模板名称', + clearable: true, + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + placeholder: ['开始时间', '结束时间'], + clearable: true, + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + minWidth: 80, + }, + { + field: 'previewPicUrls', + title: '预览图', + minWidth: 120, + cellRender: { + name: 'CellImages', + }, + }, + { + field: 'name', + title: '模板名称', + minWidth: 150, + }, + { + field: 'used', + title: '是否使用', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, + }, + }, + { + field: 'remark', + title: '备注', + minWidth: 200, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 250, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/diy/template/index.vue b/apps/web-ele/src/views/mall/promotion/diy/template/index.vue new file mode 100644 index 000000000..36f9c3b16 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/diy/template/index.vue @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/diy/template/modules/form.vue b/apps/web-ele/src/views/mall/promotion/diy/template/modules/form.vue new file mode 100644 index 000000000..8562c6c5a --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/diy/template/modules/form.vue @@ -0,0 +1,99 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/kefu/index.vue b/apps/web-ele/src/views/mall/promotion/kefu/index.vue new file mode 100644 index 000000000..bfa96fdff --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/kefu/index.vue @@ -0,0 +1,26 @@ + + + + + + 该功能支持 Vue3 + element-plus 版本! + + + + 可参考 + https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/promotion/kefu/index + 代码,pull request 贡献给我们! + + + diff --git a/apps/web-ele/src/views/mall/promotion/point/activity/data.ts b/apps/web-ele/src/views/mall/promotion/point/activity/data.ts new file mode 100644 index 000000000..276254754 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/point/activity/data.ts @@ -0,0 +1,140 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE } from '#/utils/dict'; + +/** 表单配置 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'spuId', + label: '积分商城活动商品', + component: 'Input', + componentProps: { + placeholder: '请选择商品', + }, + rules: 'required', + }, + { + fieldName: 'sort', + label: '排序', + component: 'InputNumber', + componentProps: { + placeholder: '请输入排序', + min: 0, + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 4, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + clearable: true, + dictType: DICT_TYPE.COMMON_STATUS, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '活动编号', + minWidth: 80, + }, + { + field: 'picUrl', + title: '商品图片', + minWidth: 80, + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'spuName', + title: '商品标题', + minWidth: 300, + }, + { + field: 'marketPrice', + title: '原价', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'point', + title: '兑换积分', + minWidth: 100, + }, + { + field: 'price', + title: '兑换金额', + minWidth: 100, + formatter: 'formatAmount2', + }, + { + field: 'status', + title: '活动状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'stock', + title: '库存', + minWidth: 80, + }, + { + field: 'totalStock', + title: '总库存', + minWidth: 80, + }, + { + field: 'redeemedQuantity', + title: '已兑换数量', + minWidth: 100, + slots: { default: 'redeemedQuantity' }, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 150, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/point/activity/index.vue b/apps/web-ele/src/views/mall/promotion/point/activity/index.vue new file mode 100644 index 000000000..47a8fdcb6 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/point/activity/index.vue @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + {{ getRedeemedQuantity(row) }} + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/point/activity/modules/form.vue b/apps/web-ele/src/views/mall/promotion/point/activity/modules/form.vue new file mode 100644 index 000000000..3df1c2b5a --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/point/activity/modules/form.vue @@ -0,0 +1,107 @@ + + + + + + + + 注意: + 积分活动涉及复杂的商品选择和SKU配置,当前为简化版本。 + 完整的商品选择和积分配置功能需要在后续版本中完善。 + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/rewardActivity/data.ts b/apps/web-ele/src/views/mall/promotion/rewardActivity/data.ts new file mode 100644 index 000000000..c145669bf --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/rewardActivity/data.ts @@ -0,0 +1,166 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 表单配置 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'id', + component: 'Input', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + }, + rules: 'required', + }, + { + fieldName: 'startTime', + label: '开始时间', + component: 'DatePicker', + componentProps: { + placeholder: '请选择开始时间', + showTime: true, + valueFormat: 'x', + format: 'YYYY-MM-DD HH:mm:ss', + }, + rules: 'required', + }, + { + fieldName: 'endTime', + label: '结束时间', + component: 'DatePicker', + componentProps: { + placeholder: '请选择结束时间', + showTime: true, + valueFormat: 'x', + format: 'YYYY-MM-DD HH:mm:ss', + }, + rules: 'required', + }, + { + fieldName: 'conditionType', + label: '条件类型', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE, 'number'), + }, + rules: 'required', + }, + { + fieldName: 'productScope', + label: '商品范围', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'), + }, + rules: 'required', + }, + { + fieldName: 'remark', + label: '备注', + component: 'Textarea', + componentProps: { + placeholder: '请输入备注', + rows: 4, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + clearable: true, + }, + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '活动时间', + component: 'RangePicker', + componentProps: { + placeholder: ['活动开始日期', '活动结束日期'], + clearable: true, + valueFormat: 'YYYY-MM-DD HH:mm:ss', + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'name', + title: '活动名称', + minWidth: 140, + }, + { + field: 'productScope', + title: '活动范围', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PROMOTION_PRODUCT_SCOPE }, + }, + }, + { + field: 'startTime', + title: '活动开始时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'endTime', + title: '活动结束时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'status', + title: '状态', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/rewardActivity/index.vue b/apps/web-ele/src/views/mall/promotion/rewardActivity/index.vue new file mode 100644 index 000000000..a229ebb3d --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/rewardActivity/index.vue @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/rewardActivity/modules/form.vue b/apps/web-ele/src/views/mall/promotion/rewardActivity/modules/form.vue new file mode 100644 index 000000000..f96d2940d --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/rewardActivity/modules/form.vue @@ -0,0 +1,105 @@ + + + + + + + + + + 说明: 当前为简化版本的满减送活动表单。 + 复杂的商品选择、优惠规则配置等功能已简化,仅保留基础字段配置。 + 如需完整功能,请参考原始 Element UI 版本的实现。 + + + + diff --git a/apps/web-ele/src/views/mall/promotion/seckill/activity/data.ts b/apps/web-ele/src/views/mall/promotion/seckill/activity/data.ts new file mode 100644 index 000000000..2d294e8e7 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/activity/data.ts @@ -0,0 +1,126 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '活动名称', + component: 'Input', + componentProps: { + placeholder: '请输入活动名称', + clearable: true, + }, + }, + { + fieldName: 'status', + label: '活动状态', + component: 'Select', + componentProps: { + placeholder: '请选择活动状态', + clearable: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '活动编号', + minWidth: 80, + }, + { + field: 'name', + title: '活动名称', + minWidth: 140, + }, + { + field: 'configIds', + title: '秒杀时段', + minWidth: 220, + slots: { default: 'configIds' }, + }, + { + field: 'startTime', + title: '活动时间', + minWidth: 210, + slots: { default: 'timeRange' }, + }, + { + field: 'picUrl', + title: '商品图片', + minWidth: 80, + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'spuName', + title: '商品标题', + minWidth: 300, + }, + { + field: 'marketPrice', + title: '原价', + minWidth: 100, + formatter: ({ row }) => `¥${(row.marketPrice / 100).toFixed(2)}`, + }, + { + field: 'seckillPrice', + title: '秒杀价', + minWidth: 100, + formatter: ({ row }) => { + if (!(row.products || row.products.length === 0)) { + return '¥0.00'; + } + const seckillPrice = Math.min( + ...row.products.map((item: any) => item.seckillPrice), + ); + return `¥${(seckillPrice / 100).toFixed(2)}`; + }, + }, + { + field: 'status', + title: '活动状态', + align: 'center', + minWidth: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'stock', + title: '库存', + align: 'center', + minWidth: 80, + }, + { + field: 'totalStock', + title: '总库存', + align: 'center', + minWidth: 80, + }, + { + field: 'createTime', + title: '创建时间', + align: 'center', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + align: 'center', + width: 150, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/seckill/activity/formatter.ts b/apps/web-ele/src/views/mall/promotion/seckill/activity/formatter.ts new file mode 100644 index 000000000..db1bce54d --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/activity/formatter.ts @@ -0,0 +1,34 @@ +import { formatDate } from '@vben/utils'; + +// 全局变量,用于存储配置列表 +let configList: any[] = []; + +/** 设置配置列表 */ +export function setConfigList(list: any[]) { + configList = list; +} + +/** 格式化配置名称 */ +export function formatConfigNames(configId: number): string { + const config = configList.find((item) => item.id === configId); + return config === null || config === undefined + ? '' + : `${config.name}[${config.startTime} ~ ${config.endTime}]`; +} + +/** 格式化秒杀价格 */ +export function formatSeckillPrice(products: any[]): string { + if (!products || products.length === 0) { + return '¥0.00'; + } + const seckillPrice = Math.min(...products.map((item) => item.seckillPrice)); + return `¥${(seckillPrice / 100).toFixed(2)}`; +} + +/** 格式化活动时间范围 */ +export function formatTimeRange( + startTime: Date | string, + endTime: Date | string, +): string { + return `${formatDate(startTime, 'YYYY-MM-DD')} ~ ${formatDate(endTime, 'YYYY-MM-DD')}`; +} diff --git a/apps/web-ele/src/views/mall/promotion/seckill/activity/index.vue b/apps/web-ele/src/views/mall/promotion/seckill/activity/index.vue new file mode 100644 index 000000000..6ac91ac5b --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/activity/index.vue @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + {{ formatConfigNames(configId) }} + + + + + + {{ formatTimeRange(row.startTime, row.endTime) }} + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/seckill/activity/modules/form.vue b/apps/web-ele/src/views/mall/promotion/seckill/activity/modules/form.vue new file mode 100644 index 000000000..fcdaefe2e --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/activity/modules/form.vue @@ -0,0 +1,121 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/seckill/config/data.ts b/apps/web-ele/src/views/mall/promotion/seckill/config/data.ts new file mode 100644 index 000000000..845169b6a --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/config/data.ts @@ -0,0 +1,151 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { MallSeckillConfigApi } from '#/api/mall/promotion/seckill/seckillConfig'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'name', + label: '秒杀时段名称', + component: 'Input', + rules: 'required', + }, + { + fieldName: 'startTime', + label: '开始时间点', + component: 'TimePicker', + componentProps: { + format: 'HH:mm', + valueFormat: 'HH:mm', + placeholder: '请选择开始时间点', + }, + rules: 'required', + }, + { + fieldName: 'endTime', + label: '结束时间点', + component: 'TimePicker', + componentProps: { + format: 'HH:mm', + valueFormat: 'HH:mm', + placeholder: '请选择结束时间点', + }, + rules: 'required', + }, + { + fieldName: 'sliderPicUrls', + label: '秒杀轮播图', + component: 'ImageUpload', + componentProps: { + multiple: true, + maxNumber: 5, + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: 'required', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '秒杀时段名称', + component: 'Input', + componentProps: { + placeholder: '请输入秒杀时段名称', + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + placeholder: '请选择状态', + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns( + onStatusChange?: ( + newStatus: number, + row: T, + ) => PromiseLike, +): VxeTableGridOptions['columns'] { + return [ + { + title: '秒杀时段名称', + field: 'name', + minWidth: 200, + }, + { + title: '开始时间点', + field: 'startTime', + minWidth: 120, + }, + { + title: '结束时间点', + field: 'endTime', + minWidth: 120, + }, + { + title: '秒杀轮播图', + field: 'sliderPicUrls', + minWidth: 100, + cellRender: { + name: 'CellImages', + }, + }, + { + title: '活动状态', + field: 'status', + minWidth: 100, + cellRender: { + attrs: { beforeChange: onStatusChange }, + name: 'CellSwitch', + props: { + checkedValue: 1, + checkedChildren: '启用', + unCheckedValue: 0, + unCheckedChildren: '禁用', + }, + }, + }, + { + title: '创建时间', + field: 'createTime', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/promotion/seckill/config/index.vue b/apps/web-ele/src/views/mall/promotion/seckill/config/index.vue new file mode 100644 index 000000000..6b4deae77 --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/config/index.vue @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/promotion/seckill/config/modules/form.vue b/apps/web-ele/src/views/mall/promotion/seckill/config/modules/form.vue new file mode 100644 index 000000000..ce57d7c8e --- /dev/null +++ b/apps/web-ele/src/views/mall/promotion/seckill/config/modules/form.vue @@ -0,0 +1,90 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/statistics/member/index.vue b/apps/web-ele/src/views/mall/statistics/member/index.vue new file mode 100644 index 000000000..203b6d911 --- /dev/null +++ b/apps/web-ele/src/views/mall/statistics/member/index.vue @@ -0,0 +1,34 @@ + + + + + + + 该功能支持 Vue3 + element-plus 版本! + + + + 可参考 + https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/member/index + 代码,pull request 贡献给我们! + + + diff --git a/apps/web-ele/src/views/mall/statistics/product/index.vue b/apps/web-ele/src/views/mall/statistics/product/index.vue new file mode 100644 index 000000000..cfc1f5ce3 --- /dev/null +++ b/apps/web-ele/src/views/mall/statistics/product/index.vue @@ -0,0 +1,34 @@ + + + + + + + 该功能支持 Vue3 + element-plus 版本! + + + + 可参考 + https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/product/index + 代码,pull request 贡献给我们! + + + diff --git a/apps/web-ele/src/views/mall/statistics/trade/index.vue b/apps/web-ele/src/views/mall/statistics/trade/index.vue new file mode 100644 index 000000000..dd0e7eb2b --- /dev/null +++ b/apps/web-ele/src/views/mall/statistics/trade/index.vue @@ -0,0 +1,34 @@ + + + + + + + 该功能支持 Vue3 + element-plus 版本! + + + + 可参考 + https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/mall/statistics/trade/index + 代码,pull request 贡献给我们! + + + diff --git a/apps/web-ele/src/views/mall/trade/afterSale/data.ts b/apps/web-ele/src/views/mall/trade/afterSale/data.ts new file mode 100644 index 000000000..fbe685f19 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/afterSale/data.ts @@ -0,0 +1,140 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'spuName', + label: '商品名称', + component: 'Input', + }, + { + fieldName: 'no', + label: '退款编号', + component: 'Input', + }, + { + fieldName: 'orderNo', + label: '订单编号', + component: 'Input', + }, + { + fieldName: 'status', + label: '售后状态', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_STATUS, 'number'), + }, + }, + { + fieldName: 'status', + label: '售后方式', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_WAY, 'number'), + }, + }, + { + fieldName: 'type', + label: '售后类型', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.TRADE_AFTER_SALE_TYPE, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeGridPropTypes.Columns { + return [ + { + field: 'no', + title: '退款编号', + fixed: 'left', + }, + { + field: 'orderNo', + title: '订单编号', + fixed: 'left', + slots: { default: 'orderNo' }, + }, + { + field: 'spuName', + title: '商品名称', + align: 'left', + minWidth: 200, + }, + { + field: 'picUrl', + title: '商品图片', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'properties', + title: '商品属性', + minWidth: 200, + formatter: ({ cellValue }) => { + return cellValue && cellValue.length > 0 + ? cellValue + .map((item: any) => `${item.propertyName} : ${item.valueName}`) + .join('\n') + : '-'; + }, + }, + { + field: 'refundPrice', + title: '订单金额', + formatter: 'formatAmount2', + }, + { + field: 'user.nickname', + title: '买家', + }, + { + field: 'createTime', + title: '申请时间', + formatter: 'formatDateTime', + }, + { + field: 'content', + title: '售后状态', + cellRender: { + name: 'CellDictTag', + props: { + dictType: DICT_TYPE.TRADE_AFTER_SALE_STATUS, + }, + }, + }, + { + field: 'way', + title: '售后方式', + cellRender: { + name: 'CellDictTag', + props: { + dictType: DICT_TYPE.TRADE_AFTER_SALE_WAY, + }, + }, + }, + { + title: '操作', + width: 80, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/afterSale/index.vue b/apps/web-ele/src/views/mall/trade/afterSale/index.vue new file mode 100644 index 000000000..21fcc2a85 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/afterSale/index.vue @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + {{ row.orderNo }} + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/record/data.ts b/apps/web-ele/src/views/mall/trade/brokerage/record/data.ts new file mode 100644 index 000000000..c5e6ae686 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/record/data.ts @@ -0,0 +1,135 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { fenToYuan } from '@vben/utils'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'userId', + label: '用户编号', + component: 'Input', + componentProps: { + placeholder: '请输入用户编号', + clearable: true, + }, + }, + { + fieldName: 'bizType', + label: '业务类型', + component: 'Select', + componentProps: { + placeholder: '请选择业务类型', + clearable: true, + options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE, 'number'), + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + placeholder: '请选择状态', + clearable: true, + options: getDictOptions(DICT_TYPE.BROKERAGE_RECORD_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + clearable: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + minWidth: 60, + }, + { + field: 'userId', + title: '用户编号', + minWidth: 80, + }, + { + field: 'userAvatar', + title: '头像', + minWidth: 70, + cellRender: { + name: 'CellImage', + props: { + height: 40, + width: 40, + shape: 'circle', + }, + }, + }, + { + field: 'userNickname', + title: '昵称', + minWidth: 80, + }, + { + field: 'bizType', + title: '业务类型', + minWidth: 85, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.BROKERAGE_RECORD_BIZ_TYPE }, + }, + }, + { + field: 'bizId', + title: '业务编号', + minWidth: 80, + }, + { + field: 'title', + title: '标题', + minWidth: 110, + }, + { + field: 'price', + title: '金额', + minWidth: 60, + formatter: ({ row }) => `¥${fenToYuan(row.price)}`, + }, + { + field: 'description', + title: '说明', + minWidth: 120, + }, + { + field: 'status', + title: '状态', + minWidth: 85, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.BROKERAGE_RECORD_STATUS }, + }, + }, + { + field: 'unfreezeTime', + title: '解冻时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'createTime', + title: '创建时间', + width: 180, + formatter: 'formatDateTime', + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/brokerage/record/index.vue b/apps/web-ele/src/views/mall/trade/brokerage/record/index.vue new file mode 100644 index 000000000..619cef760 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/record/index.vue @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/user/data.ts b/apps/web-ele/src/views/mall/trade/brokerage/user/data.ts new file mode 100644 index 000000000..8de25b5bf --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/user/data.ts @@ -0,0 +1,140 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { fenToYuan } from '@vben/utils'; + +import { getRangePickerDefaultProps } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'bindUserId', + label: '推广员编号', + component: 'Input', + componentProps: { + placeholder: '请输入推广员编号', + clearable: true, + }, + }, + { + fieldName: 'brokerageEnabled', + label: '推广资格', + component: 'Select', + componentProps: { + placeholder: '请选择推广资格', + clearable: true, + options: [ + { label: '有', value: true }, + { label: '无', value: false }, + ], + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + clearable: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '用户编号', + minWidth: 80, + }, + { + field: 'avatar', + title: '头像', + minWidth: 70, + cellRender: { + name: 'CellImage', + props: { + width: 24, + height: 24, + shape: 'circle', + }, + }, + }, + { + field: 'nickname', + title: '昵称', + minWidth: 80, + }, + { + field: 'brokerageUserCount', + title: '推广人数', + minWidth: 80, + }, + { + field: 'brokerageOrderCount', + title: '推广订单数量', + minWidth: 110, + }, + { + field: 'brokerageOrderPrice', + title: '推广订单金额', + minWidth: 110, + formatter: ({ row }) => `¥${fenToYuan(row.brokerageOrderPrice)}`, + }, + { + field: 'withdrawPrice', + title: '已提现金额', + minWidth: 100, + formatter: ({ row }) => `¥${fenToYuan(row.withdrawPrice)}`, + }, + { + field: 'withdrawCount', + title: '已提现次数', + minWidth: 100, + }, + { + field: 'price', + title: '未提现金额', + minWidth: 100, + formatter: ({ row }) => `¥${fenToYuan(row.price)}`, + }, + { + field: 'frozenPrice', + title: '冻结中佣金', + minWidth: 100, + formatter: ({ row }) => `¥${fenToYuan(row.frozenPrice)}`, + }, + { + field: 'brokerageEnabled', + title: '推广资格', + minWidth: 80, + slots: { default: 'brokerageEnabled' }, + }, + { + field: 'brokerageTime', + title: '成为推广员时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'bindUserId', + title: '上级推广员编号', + minWidth: 150, + }, + { + field: 'bindUserTime', + title: '推广员绑定时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 150, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/brokerage/user/index.vue b/apps/web-ele/src/views/mall/trade/brokerage/user/index.vue new file mode 100644 index 000000000..b9e510480 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/user/index.vue @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue new file mode 100644 index 000000000..8706aa32f --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/order-list-modal.vue @@ -0,0 +1,185 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-create-form.vue b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-create-form.vue new file mode 100644 index 000000000..2df46722f --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-create-form.vue @@ -0,0 +1,181 @@ + + + + + + 分销员编号: + + + + + + + + 上级推广人编号: + + + + + + + + + + + + + + + + + {{ userInfo.user?.nickname }} + + + + + + + + + {{ userInfo.bindUser?.nickname }} + + + + 有 + + 无 + + + {{ formatDate(userInfo.bindUser?.brokerageTime) }} + + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue new file mode 100644 index 000000000..39ffbd7ef --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-list-modal.vue @@ -0,0 +1,160 @@ + + + + + + + 有 + 无 + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-update-form.vue b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-update-form.vue new file mode 100644 index 000000000..8e850c625 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/user/modules/user-update-form.vue @@ -0,0 +1,136 @@ + + + + + + 推广员编号: + + + + + + + + + + + + + + + + {{ bindUser.nickname }} + + + 有 + 无 + + + {{ formatDate(bindUser.brokerageTime) }} + + + + diff --git a/apps/web-ele/src/views/mall/trade/brokerage/withdraw/data.ts b/apps/web-ele/src/views/mall/trade/brokerage/withdraw/data.ts new file mode 100644 index 000000000..f72e5281d --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/withdraw/data.ts @@ -0,0 +1,145 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { DICT_TYPE, getDictOptions, getRangePickerDefaultProps } from '#/utils'; + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'userId', + label: '用户编号', + component: 'Input', + componentProps: { + placeholder: '请输入用户编号', + clearable: true, + }, + }, + { + fieldName: 'type', + label: '提现类型', + component: 'Select', + componentProps: { + placeholder: '请选择提现类型', + clearable: true, + options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, 'number'), + }, + }, + { + fieldName: 'userAccount', + label: '账号', + component: 'Input', + componentProps: { + placeholder: '请输入账号', + clearable: true, + }, + }, + { + fieldName: 'userName', + label: '真实名字', + component: 'Input', + componentProps: { + placeholder: '请输入真实名字', + clearable: true, + }, + }, + { + fieldName: 'bankName', + label: '提现银行', + component: 'Select', + componentProps: { + placeholder: '请选择提现银行', + clearable: true, + options: getDictOptions(DICT_TYPE.BROKERAGE_BANK_NAME, 'string'), + }, + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + placeholder: '请选择状态', + clearable: true, + options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '申请时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + clearable: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + minWidth: 80, + }, + { + field: 'userId', + title: '用户编号:', + minWidth: 80, + }, + { + field: 'userNickname', + title: '用户昵称:', + minWidth: 80, + }, + { + field: 'price', + title: '提现金额', + minWidth: 80, + formatter: 'formatAmount2', + }, + { + field: 'feePrice', + title: '提现手续费', + minWidth: 80, + formatter: 'formatAmount2', + }, + { + field: 'type', + title: '提现方式', + minWidth: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.BROKERAGE_WITHDRAW_TYPE }, + }, + }, + { + title: '提现信息', + minWidth: 200, + slots: { default: 'withdraw-info' }, + }, + { + field: 'createTime', + title: '申请时间', + minWidth: 180, + formatter: 'formatDateTime', + }, + { + field: 'remark', + title: '备注', + minWidth: 120, + }, + { + title: '状态', + minWidth: 200, + slots: { default: 'status-info' }, + }, + { + title: '操作', + width: 150, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/brokerage/withdraw/index.vue b/apps/web-ele/src/views/mall/trade/brokerage/withdraw/index.vue new file mode 100644 index 000000000..d6cd5921f --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/brokerage/withdraw/index.vue @@ -0,0 +1,195 @@ + + + + + + + - + + 账号:{{ row.userAccount }} + 真实姓名:{{ row.userName }} + + 银行名称:{{ row.bankName }} + 开户地址:{{ row.bankAddress }} + + + 收款码: + + + + + + + + + + 时间:{{ formatDateTime(row.auditTime) }} + + + 审核原因:{{ row.auditReason }} + + + 转账失败原因:{{ row.transferErrorMsg }} + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/config/data.ts b/apps/web-ele/src/views/mall/trade/config/data.ts new file mode 100644 index 000000000..9c35219fc --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/config/data.ts @@ -0,0 +1,243 @@ +import type { VbenFormSchema } from '#/adapter/form'; + +import { DICT_TYPE, getDictOptions } from '#/utils'; + +/** 售后表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + component: 'Input', + fieldName: 'type', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + fieldName: 'afterSaleRefundReasons', + label: '退款理由', + component: 'Select', + componentProps: { + mode: 'tags', + placeholder: '请直接输入退款理由', + class: 'w-full', + }, + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'afterSale', + }, + }, + { + fieldName: 'afterSaleReturnReasons', + label: '退货理由', + component: 'Select', + componentProps: { + mode: 'tags', + placeholder: '请直接输入退货理由', + class: 'w-full', + }, + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'afterSale', + }, + }, + { + fieldName: 'deliveryExpressFreeEnabled', + label: '启用包邮', + component: 'Switch', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'delivery', + }, + description: '商城是否启用全场包邮', + }, + { + fieldName: 'deliveryExpressFreePrice', + label: '满额包邮', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'delivery', + }, + description: '商城商品满多少金额即可包邮,单位:元', + }, + { + fieldName: 'deliveryPickUpEnabled', + label: '启用门店自提', + component: 'Switch', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'delivery', + }, + }, + { + fieldName: 'brokerageEnabled', + label: '启用分佣', + component: 'Switch', + description: '商城是否开启分销模式', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + }, + { + fieldName: 'brokerageEnabledCondition', + label: '分佣模式', + component: 'RadioGroup', + componentProps: { + options: getDictOptions( + DICT_TYPE.BROKERAGE_ENABLED_CONDITION, + 'number', + ), + buttonStyle: 'solid', + optionType: 'button', + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: + '人人分销:每个用户都可以成为推广员 \n 单级分销:每个用户只能有一个上级推广员', + }, + { + fieldName: 'brokerageBindMode', + label: '分销关系绑定', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.BROKERAGE_BIND_MODE, 'number'), + buttonStyle: 'solid', + optionType: 'button', + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: + '首次绑定:只要用户没有推广人,随时都可以绑定推广关系 \n 注册绑定:只有新用户注册时或首次进入系统时才可以绑定推广关系', + }, + { + fieldName: 'brokeragePosterUrls', + label: '分销海报图', + component: 'ImageUpload', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: '个人中心分销海报图片,建议尺寸 600x1000', + }, + { + fieldName: 'brokerageFirstPercent', + label: '一级返佣比例', + component: 'InputNumber', + componentProps: { + min: 0, + max: 100, + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: '订单交易成功后给推广人返佣的百分比', + }, + { + fieldName: 'brokerageSecondPercent', + label: '二级返佣比例', + component: 'InputNumber', + componentProps: { + min: 0, + max: 100, + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: '订单交易成功后给推广人的推荐人返佣的百分比', + }, + { + fieldName: 'brokerageFrozenDays', + label: '佣金冻结天数', + component: 'InputNumber', + componentProps: { + min: 0, + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: + '防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天', + }, + { + fieldName: 'brokerageWithdrawMinPrice', + label: '提现最低金额', + component: 'InputNumber', + componentProps: { + min: 0, + precision: 2, + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: '用户提现最低金额限制,单位:元', + }, + { + fieldName: 'brokerageWithdrawFeePercent', + label: '提现手续费', + component: 'InputNumber', + componentProps: { + min: 0, + max: 100, + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: + '提现手续费百分比,范围 0-100,0 为无提现手续费。例:设置 10,即收取 10% 手续费,提现10 元,到账 9 元,1 元手续费', + }, + { + fieldName: 'brokerageWithdrawTypes', + label: '提现方式', + component: 'CheckboxGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, 'number'), + class: 'w-full', + }, + rules: 'required', + dependencies: { + triggerFields: ['type'], + show: (values) => values.type === 'brokerage', + }, + description: '商城开通提现的付款方式', + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/config/index.vue b/apps/web-ele/src/views/mall/trade/config/index.vue new file mode 100644 index 000000000..9c1339d27 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/config/index.vue @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/express/data.ts b/apps/web-ele/src/views/mall/trade/delivery/express/data.ts new file mode 100644 index 000000000..908cf3e1e --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/express/data.ts @@ -0,0 +1,130 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; +import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + component: 'Input', + fieldName: 'code', + label: '公司编码', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'name', + label: '公司名称', + rules: 'required', + }, + { + component: 'ImageUpload', + fieldName: 'logo', + label: '公司 logo', + rules: 'required', + }, + { + fieldName: 'sort', + label: '显示顺序', + component: 'InputNumber', + componentProps: { + min: 0, + }, + rules: 'required', + }, + { + fieldName: 'status', + label: '开启状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '快递公司名称', + component: 'Input', + }, + { + fieldName: 'code', + label: '快递公司编号', + component: 'Input', + }, + { + fieldName: 'status', + label: '状态', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + }, + { + field: 'code', + title: '公司编码', + }, + { + field: 'name', + title: '公司名称', + }, + { + field: 'logo', + title: '公司 logo', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'sort', + title: '显示顺序', + }, + { + field: 'status', + title: '状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 130, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/delivery/express/index.vue b/apps/web-ele/src/views/mall/trade/delivery/express/index.vue new file mode 100644 index 000000000..97ba38045 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/express/index.vue @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/express/modules/form.vue b/apps/web-ele/src/views/mall/trade/delivery/express/modules/form.vue new file mode 100644 index 000000000..5cf29b3ba --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/express/modules/form.vue @@ -0,0 +1,89 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/data.ts b/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/data.ts new file mode 100644 index 000000000..b243fd883 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/data.ts @@ -0,0 +1,102 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; +import { CommonStatusEnum, DICT_TYPE, getDictOptions } from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + component: 'Input', + fieldName: 'name', + label: '模板名称', + rules: 'required', + }, + { + fieldName: 'chargeMode', + label: '计费方式', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + { + fieldName: 'sort', + label: '显示顺序', + component: 'InputNumber', + componentProps: { + min: 0, + }, + rules: 'required', + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'name', + label: '模板名称', + component: 'Input', + }, + { + fieldName: 'chargeMode', + label: '计费方式', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.EXPRESS_CHARGE_MODE, 'number'), + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + }, + { + field: 'name', + title: '模板名称', + }, + { + field: 'chargeMode', + title: '计费方式', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.EXPRESS_CHARGE_MODE }, + }, + }, + { + field: 'sort', + title: '显示顺序', + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 130, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/index.vue b/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/index.vue new file mode 100644 index 000000000..76751bfa2 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/index.vue @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/modules/form.vue b/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/modules/form.vue new file mode 100644 index 000000000..715bfedf9 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/expressTemplate/modules/form.vue @@ -0,0 +1,91 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/data.ts b/apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/data.ts new file mode 100644 index 000000000..f68f275fa --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/data.ts @@ -0,0 +1,127 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeGridPropTypes } from '#/adapter/vxe-table'; +import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore'; + +import { ref } from 'vue'; + +import { getSimpleDeliveryPickUpStoreList } from '#/api/mall/trade/delivery/pickUpStore'; +import { + DeliveryTypeEnum, + DICT_TYPE, + getRangePickerDefaultProps, +} from '#/utils'; + +const pickUpStoreList = ref([]); + +getSimpleDeliveryPickUpStoreList().then((res) => { + pickUpStoreList.value = res; +}); + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + { + fieldName: 'pickUpStoreId', + label: '自提门店', + component: 'ApiSelect', + componentProps: { + api: getSimpleDeliveryPickUpStoreList, + fieldNames: { + label: 'name', + value: 'id', + }, + }, + dependencies: { + triggerFields: ['deliveryType'], + trigger: (values) => + values.deliveryType === DeliveryTypeEnum.PICK_UP.type, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeGridPropTypes.Columns { + return [ + { + field: 'no', + title: '订单号', + fixed: 'left', + minWidth: 180, + }, + { + field: 'user.nickname', + title: '用户信息', + minWidth: 100, + }, + { + field: 'brokerageUser.nickname', + title: '推荐人信息', + minWidth: 100, + }, + { + field: 'spuName', + title: '商品信息', + minWidth: 100, + formatter: ({ row }) => { + if (row.items.length > 1) { + return row.items.map((item: any) => item.spuName).join(','); + } + }, + }, + { + field: 'payPrice', + title: '实付金额(元)', + formatter: 'formatAmount2', + minWidth: 180, + }, + { + field: 'storeStaffName', + title: '核销员', + minWidth: 160, + }, + { + field: 'pickUpStoreId', + title: '核销门店', + minWidth: 160, + formatter: ({ row }) => { + return pickUpStoreList.value.find( + (item) => item.id === row.pickUpStoreId, + )?.name; + }, + }, + { + field: 'payStatus', + title: '支付状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, + }, + minWidth: 80, + }, + { + field: 'status', + title: '订单状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.TRADE_ORDER_STATUS }, + }, + minWidth: 80, + }, + { + field: 'createTime', + title: '下单时间', + formatter: 'formatDateTime', + minWidth: 160, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/index.vue b/apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/index.vue new file mode 100644 index 000000000..72dc3ceed --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/pickUpOrder/index.vue @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/data.ts b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/data.ts new file mode 100644 index 000000000..ac3e08287 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/data.ts @@ -0,0 +1,245 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; + +import { z } from '#/adapter/form'; +import { getAreaTree } from '#/api/system/area'; +import { getSimpleUserList } from '#/api/system/user'; +import { + CommonStatusEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +/** 新增/修改的表单 */ +export function useFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + component: 'ImageUpload', + fieldName: 'logo', + label: '门店logo', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'name', + label: '门店名称', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'phone', + label: '门店手机', + rules: 'mobileRequired', + }, + { + component: 'Textarea', + fieldName: 'introduction', + label: '门店简介', + }, + { + fieldName: 'areaId', + label: '地址', + component: 'ApiTreeSelect', + componentProps: { + api: () => getAreaTree(), + fieldNames: { label: 'name', value: 'id', children: 'children' }, + }, + }, + { + component: 'Input', + fieldName: 'detailAddress', + label: '详细地址', + rules: 'required', + }, + { + component: 'TimePicker', + fieldName: 'openingTime', + label: '营业开始时间', + rules: 'required', + }, + { + component: 'TimePicker', + fieldName: 'closingTime', + label: '营业结束时间', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'longitude', + label: '经度', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'latitude', + label: '纬度', + rules: 'required', + }, + { + component: 'Input', + fieldName: 'getGeo', + label: '获取经纬度', + }, + { + fieldName: 'status', + label: '门店状态', + component: 'RadioGroup', + componentProps: { + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + buttonStyle: 'solid', + optionType: 'button', + }, + rules: z.number().default(CommonStatusEnum.ENABLE), + }, + ]; +} + +/** 绑定店员的表单 */ +export function useBindFormSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'id', + dependencies: { + triggerFields: [''], + show: () => false, + }, + }, + { + component: 'Input', + fieldName: 'name', + label: '门店名称', + dependencies: { + triggerFields: ['id'], + disabled: true, + }, + }, + { + component: 'ApiSelect', + fieldName: 'verifyUserIds', + label: '门店店员', + rules: 'required', + componentProps: { + api: () => getSimpleUserList(), + fieldNames: { label: 'nickname', value: 'id' }, + mode: 'tags', + allowClear: true, + }, + }, + { + component: 'Select', + fieldName: 'verifyUsers', + label: '店员列表', + rules: 'required', + componentProps: { + options: [], + mode: 'tags', + }, + dependencies: { + triggerFields: ['verifyUserIds'], + trigger(values, form) { + form.setFieldValue('verifyUsers', values.verifyUserIds); + }, + disabled: true, + }, + }, + ]; +} + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'phone', + label: '门店手机', + component: 'Input', + }, + { + fieldName: 'name', + label: '门店名称', + component: 'Input', + }, + { + fieldName: 'status', + label: '门店状态', + component: 'Select', + componentProps: { + allowClear: true, + options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'), + }, + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + ]; +} + +/** 列表的字段 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: '编号', + }, + { + field: 'logo', + title: '门店logo', + cellRender: { + name: 'CellImage', + }, + }, + { + field: 'name', + title: '门店名称', + }, + { + field: 'phone', + title: '门店手机', + }, + { + field: 'detailAddress', + title: '地址', + }, + { + field: 'openingTime', + title: '营业时间', + formatter: ({ row }) => { + return `${row.openingTime} ~ ${row.closingTime}`; + }, + }, + { + field: 'status', + title: '开启状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.COMMON_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + formatter: 'formatDateTime', + }, + { + title: '操作', + width: 200, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/index.vue b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/index.vue new file mode 100644 index 000000000..769decd18 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/index.vue @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue new file mode 100644 index 000000000..6e6f792fe --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/bind-form.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/form.vue b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/form.vue new file mode 100644 index 000000000..2a4afeaac --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/delivery/pickUpStore/modules/form.vue @@ -0,0 +1,89 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/order/data.ts b/apps/web-ele/src/views/mall/trade/order/data.ts new file mode 100644 index 000000000..6fc928244 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/order/data.ts @@ -0,0 +1,214 @@ +import type { VbenFormSchema } from '#/adapter/form'; +import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import type { MallDeliveryPickUpStoreApi } from '#/api/mall/trade/delivery/pickUpStore'; + +import { ref } from 'vue'; + +import { getSimpleDeliveryExpressList } from '#/api/mall/trade/delivery/express'; +import { getSimpleDeliveryPickUpStoreList } from '#/api/mall/trade/delivery/pickUpStore'; +import { + DeliveryTypeEnum, + DICT_TYPE, + getDictOptions, + getRangePickerDefaultProps, +} from '#/utils'; + +const pickUpStoreList = ref([]); + +getSimpleDeliveryPickUpStoreList().then((res) => { + pickUpStoreList.value = res; +}); + +/** 列表的搜索表单 */ +export function useGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'status', + label: '订单状态', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.TRADE_ORDER_STATUS, 'number'), + }, + }, + { + fieldName: 'payChannelCode', + label: '支付方式', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.PAY_CHANNEL_CODE, 'number'), + }, + }, + { + fieldName: 'name', + label: '品牌名称', + component: 'Input', + }, + { + fieldName: 'createTime', + label: '创建时间', + component: 'RangePicker', + componentProps: { + ...getRangePickerDefaultProps(), + allowClear: true, + }, + }, + { + fieldName: 'terminal', + label: '订单来源', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.TERMINAL, 'number'), + }, + }, + { + fieldName: 'deliveryType', + label: '配送方式', + component: 'Select', + componentProps: { + options: getDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE, 'number'), + }, + }, + { + fieldName: 'logisticsId', + label: '快递公司', + component: 'ApiSelect', + componentProps: { + api: getSimpleDeliveryExpressList, + labelField: 'name', + valueField: 'id', + }, + dependencies: { + triggerFields: ['deliveryType'], + show: (values) => values.deliveryType === DeliveryTypeEnum.EXPRESS.type, + }, + }, + { + fieldName: 'pickUpStoreId', + label: '自提门店', + component: 'ApiSelect', + componentProps: { + api: getSimpleDeliveryPickUpStoreList, + labelField: 'name', + valueField: 'id', + }, + dependencies: { + triggerFields: ['deliveryType'], + show: (values) => values.deliveryType === DeliveryTypeEnum.PICK_UP.type, + }, + }, + { + fieldName: 'pickUpVerifyCode', + label: '核销码', + component: 'Input', + dependencies: { + triggerFields: ['deliveryType'], + show: (values) => values.deliveryType === DeliveryTypeEnum.PICK_UP.type, + }, + }, + ]; +} + +/** 表格列配置 */ +export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { + type: 'expand', + width: 80, + slots: { content: 'expand_content' }, + fixed: 'left', + }, + { + field: 'no', + title: '订单号', + fixed: 'left', + minWidth: 180, + }, + { + field: 'createTime', + title: '下单时间', + formatter: 'formatDateTime', + minWidth: 160, + }, + { + field: 'terminal', + title: '订单来源', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.TERMINAL }, + }, + minWidth: 120, + }, + { + field: 'payChannelCode', + title: '支付方式', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.PAY_CHANNEL_CODE }, + }, + minWidth: 120, + }, + { + field: 'payTime', + title: '支付时间', + formatter: 'formatDateTime', + minWidth: 160, + }, + { + field: 'type', + title: '订单类型', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.TRADE_ORDER_TYPE }, + }, + minWidth: 80, + }, + + { + field: 'payPrice', + title: '实际支付', + formatter: 'formatAmount2', + minWidth: 180, + }, + { + field: 'user', + title: '买家/收货人', + formatter: ({ row }) => { + if (row.deliveryType === DeliveryTypeEnum.EXPRESS.type) { + return `买家:${row.user?.nickname} / 收货人: ${row.receiverName} ${row.receiverMobile}${row.receiverAreaName}${row.receiverDetailAddress}`; + } + if (row.deliveryType === DeliveryTypeEnum.PICK_UP.type) { + return `门店名称:${pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)?.name} / + 门店手机:${pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)?.phone} / + 自提门店:${pickUpStoreList.value.find((item) => item.id === row.pickUpStoreId)?.detailAddress} + `; + } + return ''; + }, + minWidth: 180, + }, + { + field: 'deliveryType', + title: '配送方式', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.TRADE_DELIVERY_TYPE }, + }, + minWidth: 80, + }, + { + field: 'status', + title: '订单状态', + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.TRADE_ORDER_STATUS }, + }, + minWidth: 80, + }, + { + title: '操作', + width: 180, + fixed: 'right', + slots: { default: 'actions' }, + }, + ]; +} diff --git a/apps/web-ele/src/views/mall/trade/order/index.vue b/apps/web-ele/src/views/mall/trade/order/index.vue new file mode 100644 index 000000000..5cf91f1c4 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/order/index.vue @@ -0,0 +1,217 @@ + + + + + + + + + + + + + + + + + + + {{ item.spuName }} + + {{ property.propertyName }}: {{ property.valueName }} + + + + 原价:{{ fenToYuan(item.price) }} 元 / 数量:{{ + item.count + }}个 + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/trade/order/modules/delevery-form.vue b/apps/web-ele/src/views/mall/trade/order/modules/delevery-form.vue new file mode 100644 index 000000000..92fd9fec4 --- /dev/null +++ b/apps/web-ele/src/views/mall/trade/order/modules/delevery-form.vue @@ -0,0 +1,131 @@ + + + + + + + diff --git a/apps/web-ele/src/views/member/user/components/user-after-sale-list.vue b/apps/web-ele/src/views/member/user/components/user-after-sale-list.vue new file mode 100644 index 000000000..239b92c88 --- /dev/null +++ b/apps/web-ele/src/views/member/user/components/user-after-sale-list.vue @@ -0,0 +1,147 @@ + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/member/user/components/user-brokerage-list.vue b/apps/web-ele/src/views/member/user/components/user-brokerage-list.vue new file mode 100644 index 000000000..7078359be --- /dev/null +++ b/apps/web-ele/src/views/member/user/components/user-brokerage-list.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/apps/web-ele/src/views/member/user/components/user-coupon-list.vue b/apps/web-ele/src/views/member/user/components/user-coupon-list.vue new file mode 100644 index 000000000..591093c45 --- /dev/null +++ b/apps/web-ele/src/views/member/user/components/user-coupon-list.vue @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/member/user/components/user-favorite-list.vue b/apps/web-ele/src/views/member/user/components/user-favorite-list.vue new file mode 100644 index 000000000..2623630ae --- /dev/null +++ b/apps/web-ele/src/views/member/user/components/user-favorite-list.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/apps/web-ele/src/views/member/user/components/user-order-list.vue b/apps/web-ele/src/views/member/user/components/user-order-list.vue new file mode 100644 index 000000000..5f53fd585 --- /dev/null +++ b/apps/web-ele/src/views/member/user/components/user-order-list.vue @@ -0,0 +1,270 @@ + + + + + + + + + + + + + {{ item.spuName }} + + {{ property.propertyName }}: {{ property.valueName }} + + + + 原价:{{ fenToYuan(item.price) }} 元 / 数量:{{ + item.count + }} + 个 + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/member/user/modules/detail.vue b/apps/web-ele/src/views/member/user/modules/detail.vue index e2f31353b..54f556795 100644 --- a/apps/web-ele/src/views/member/user/modules/detail.vue +++ b/apps/web-ele/src/views/member/user/modules/detail.vue @@ -16,9 +16,14 @@ import { $t } from '#/locales'; import UserAccountInfo from '../components/user-account-info.vue'; import UserAddressList from '../components/user-address-list.vue'; +import UserAfterSaleList from '../components/user-after-sale-list.vue'; import UserBalanceList from '../components/user-balance-list.vue'; import UserBasicInfo from '../components/user-basic-info.vue'; +import UserBrokerageList from '../components/user-brokerage-list.vue'; +import UserCouponList from '../components/user-coupon-list.vue'; import UserExperienceRecordList from '../components/user-experience-record-list.vue'; +import UserFavoriteList from '../components/user-favorite-list.vue'; +import UserOrderList from '../components/user-order-list.vue'; import UserPointList from '../components/user-point-list.vue'; import UserSignList from '../components/user-sign-list.vue'; import Form from './form.vue'; @@ -102,31 +107,31 @@ onMounted(async () => { - 订单管理 + - 售后管理 + - 收藏记录 + - 优惠劵 + - 推广用户 + diff --git a/packages/effects/common-ui/src/components/json-viewer/style.scss b/packages/effects/common-ui/src/components/json-viewer/style.scss index bcc2bfdce..47acb891f 100644 --- a/packages/effects/common-ui/src/components/json-viewer/style.scss +++ b/packages/effects/common-ui/src/components/json-viewer/style.scss @@ -14,8 +14,8 @@ padding: 0 4px 2px; font-size: 0.9em; line-height: 0.9; - color: hsl(var(--secondary-foreground)); vertical-align: 2px; + color: hsl(var(--secondary-foreground)); cursor: pointer; user-select: none; background-color: hsl(var(--secondary)); diff --git a/packages/effects/common-ui/src/components/resize/resize.vue b/packages/effects/common-ui/src/components/resize/resize.vue index aaf89eaf2..e89ea8b8d 100644 --- a/packages/effects/common-ui/src/components/resize/resize.vue +++ b/packages/effects/common-ui/src/components/resize/resize.vue @@ -1072,8 +1072,8 @@ watch( box-sizing: border-box; width: 100%; height: 100%; - content: ''; outline: 1px dashed #d6d6d6; + content: ''; } .resize-stick { diff --git a/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue b/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue index 77f330c58..9ee4bceb5 100644 --- a/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue +++ b/packages/effects/layouts/src/widgets/theme-toggle/theme-button.vue @@ -132,8 +132,8 @@ function toggleTheme(event: MouseEvent) { &__sun { @apply fill-foreground/90 stroke-none; - transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1); transform-origin: center center; + transition: transform 1.6s cubic-bezier(0.25, 0, 0.2, 1); &:hover > svg > & { @apply fill-foreground/90; @@ -143,10 +143,10 @@ function toggleTheme(event: MouseEvent) { &__sun-beams { @apply stroke-foreground/90 stroke-[2px]; + transform-origin: center center; transition: transform 1.6s cubic-bezier(0.5, 1.5, 0.75, 1.25), opacity 0.6s cubic-bezier(0.25, 0, 0.3, 1); - transform-origin: center center; &:hover > svg > & { @apply stroke-foreground; -- Gitee From f0516fa857090834aae59e226109fcfc0681b8e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=83=E8=B4=A7?= <252048765@qq.com> Date: Sun, 6 Jul 2025 21:27:44 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=95=86?= =?UTF-8?q?=E5=93=81=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E5=90=AB=E5=95=86=E5=93=81=E5=88=86=E7=B1=BB=E3=80=81=E5=93=81?= =?UTF-8?q?=E7=89=8C=E3=80=81SPU=E7=AE=A1=E7=90=86=E5=8F=8A=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E8=A1=A8=E5=8D=95=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-ele/src/adapter/component/index.ts | 24 + apps/web-ele/src/adapter/vxe-table.ts | 10 +- apps/web-ele/src/api/mall/product/category.ts | 7 + .../src/components/upload/image-upload.vue | 52 +- .../web-ele/src/router/routes/modules/mall.ts | 76 +++ apps/web-ele/src/utils/bean.ts | 17 + apps/web-ele/src/utils/formatNum.ts | 38 ++ apps/web-ele/src/utils/index.ts | 4 + apps/web-ele/src/utils/is.ts | 117 ++++ apps/web-ele/src/utils/tree.ts | 440 +++++++++++++ .../src/views/mall/product/brand/data.ts | 3 + .../product/spu/components/delivery-form.vue | 84 +++ .../spu/components/description-form.vue | 66 ++ .../mall/product/spu/components/info-form.vue | 138 ++++ .../mall/product/spu/components/model.d.ts | 62 ++ .../product/spu/components/other-form.vue | 86 +++ .../spu/components/product-fttributes.vue | 195 ++++++ .../components/product-property-add-form.vue | 130 ++++ .../mall/product/spu/components/sku-form.vue | 192 ++++++ .../mall/product/spu/components/sku-list.vue | 613 ++++++++++++++++++ .../views/mall/product/spu/modules/form.vue | 128 +++- 21 files changed, 2465 insertions(+), 17 deletions(-) create mode 100644 apps/web-ele/src/router/routes/modules/mall.ts create mode 100644 apps/web-ele/src/utils/bean.ts create mode 100644 apps/web-ele/src/utils/formatNum.ts create mode 100644 apps/web-ele/src/utils/is.ts create mode 100644 apps/web-ele/src/utils/tree.ts create mode 100644 apps/web-ele/src/views/mall/product/spu/components/delivery-form.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/description-form.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/info-form.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/model.d.ts create mode 100644 apps/web-ele/src/views/mall/product/spu/components/other-form.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/product-property-add-form.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/sku-form.vue create mode 100644 apps/web-ele/src/views/mall/product/spu/components/sku-list.vue diff --git a/apps/web-ele/src/adapter/component/index.ts b/apps/web-ele/src/adapter/component/index.ts index 411cf50f5..aca6d3181 100644 --- a/apps/web-ele/src/adapter/component/index.ts +++ b/apps/web-ele/src/adapter/component/index.ts @@ -126,6 +126,12 @@ const ElUpload = defineAsyncComponent(() => import('element-plus/es/components/upload/style/css'), ]).then(([res]) => res.ElUpload), ); +const ElCascader = defineAsyncComponent(() => + Promise.all([ + import('element-plus/es/components/cascader/index'), + import('element-plus/es/components/cascader/style/css'), + ]).then(([res]) => res.ElCascader), +); const withDefaultPlaceholder = ( component: T, @@ -185,6 +191,7 @@ export type ComponentType = | 'TimePicker' | 'TreeSelect' | 'Upload' + | 'ApiCascader' | BaseFormComponentType; async function initComponentAdapter() { @@ -204,6 +211,23 @@ async function initComponentAdapter() { visibleEvent: 'onVisibleChange', }, ), + ApiCascader: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiCascader', + }, + 'select', + { + component: ElCascader, + props: { + props: { + label: 'label', + value: 'value', + children: 'children', + }, + }, + }, + ), ApiTreeSelect: withDefaultPlaceholder( { ...ApiComponent, diff --git a/apps/web-ele/src/adapter/vxe-table.ts b/apps/web-ele/src/adapter/vxe-table.ts index 5bf8b3137..a2938b977 100644 --- a/apps/web-ele/src/adapter/vxe-table.ts +++ b/apps/web-ele/src/adapter/vxe-table.ts @@ -75,10 +75,16 @@ setupVbenVxeTable({ // 表格配置项可以用 cellRender: { name: 'CellImage' }, vxeUI.renderer.add('CellImage', { - renderTableDefault(_renderOpts, params) { + renderTableDefault(renderOpts, params) { + const { props } = renderOpts; const { column, row } = params; const src = row[column.field]; - return h(ElImage, { src, previewSrcList: [src] }); + return h(ElImage, { + src, + previewSrcList: [src], + class: props?.class, + previewTeleported: true, + }); }, }); diff --git a/apps/web-ele/src/api/mall/product/category.ts b/apps/web-ele/src/api/mall/product/category.ts index f30d6c6b7..3ca05b0f9 100644 --- a/apps/web-ele/src/api/mall/product/category.ts +++ b/apps/web-ele/src/api/mall/product/category.ts @@ -49,3 +49,10 @@ export function getCategoryList(params: any) { }, ); } + +// 获得商品分类列表 +export function getCategorySimpleList() { + return requestClient.get( + '/product/category/list-all-simple', + ); +} diff --git a/apps/web-ele/src/components/upload/image-upload.vue b/apps/web-ele/src/components/upload/image-upload.vue index 174df55fc..edb3fc9c0 100644 --- a/apps/web-ele/src/components/upload/image-upload.vue +++ b/apps/web-ele/src/components/upload/image-upload.vue @@ -48,10 +48,14 @@ const props = withDefaults( resultField?: string; // 是否显示下面的描述 showDescription?: boolean; - value?: string | string[]; + modelValue?: string | string[]; + // 上传框宽度 + width?: string | number; + // 上传框高度 + height?: string | number; }>(), { - value: () => [], + modelValue: () => [], directory: undefined, disabled: false, listType: 'picture-card', @@ -63,11 +67,13 @@ const props = withDefaults( api: undefined, resultField: '', showDescription: true, + width: '', + height: '', }, ); -const emit = defineEmits(['change', 'update:value', 'delete']); -const { accept, helpText, maxNumber, maxSize } = toRefs(props); +const emit = defineEmits(['change', 'update:modelValue', 'delete']); +const { accept, helpText, maxNumber, maxSize, width, height } = toRefs(props); const isInnerOperate = ref(false); const { getStringAccept } = useUploadType({ acceptRef: accept, @@ -82,7 +88,7 @@ const isActMsg = ref(true); // 文件类型错误提示 const isFirstRender = ref(true); // 是否第一次渲染 watch( - () => props.value, + () => props.modelValue, async (v) => { if (isInnerOperate.value) { isInnerOperate.value = false; @@ -101,7 +107,7 @@ watch( return { uid: -i, name: item.slice(Math.max(0, item.lastIndexOf('/') + 1)), - status: UploadResultStatus.DONE, + status: UploadResultStatus.SUCCESS, url: item, } as UploadFile; } else if (item && isObject(item)) { @@ -109,7 +115,7 @@ watch( return { uid: file.uid || -i, name: file.name || '', - status: UploadResultStatus.DONE, + status: UploadResultStatus.SUCCESS, url: file.url, } as UploadFile; } @@ -154,7 +160,7 @@ const handleRemove = async (file: UploadFile) => { index !== -1 && fileList.value.splice(index, 1); const value = getValue(); isInnerOperate.value = true; - emit('update:value', value); + emit('update:modelValue', value); emit('change', value); emit('delete', file); } @@ -204,7 +210,7 @@ async function customRequest(options: UploadRequestOptions) { // 更新文件 const value = getValue(); isInnerOperate.value = true; - emit('update:value', value); + emit('update:modelValue', value); emit('change', value); } catch (error: any) { console.error(error); @@ -213,13 +219,14 @@ async function customRequest(options: UploadRequestOptions) { } function getValue() { + console.log(fileList.value); const list = (fileList.value || []) - .filter((item) => item?.status === UploadResultStatus.DONE) + .filter((item) => item?.status === UploadResultStatus.SUCCESS) .map((item: any) => { if (item?.response && props?.resultField) { return item?.response; } - return item?.url || item?.response?.url || item?.response; + return item?.response?.url || item?.response; }); // add by 芋艿:【特殊】单个文件的情况,获取首个元素,保证返回的是 String 类型 if (props.maxNumber === 1) { @@ -243,10 +250,11 @@ function getValue() { :multiple="multiple" :on-preview="handlePreview" :on-remove="handleRemove" + :class="width || height ? 'custom-upload' : ''" > {{ $t('ui.upload.imgUpload') }} @@ -262,4 +270,22 @@ function getValue() { .ant-upload-select-picture-card { @apply flex items-center justify-center; } + +.custom-upload .el-upload { + width: auto !important; + height: auto !important; +} + +.custom-upload .el-upload--picture-card { + width: auto !important; + height: auto !important; + line-height: normal !important; +} + +.custom-upload .upload-content { + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} diff --git a/apps/web-ele/src/router/routes/modules/mall.ts b/apps/web-ele/src/router/routes/modules/mall.ts new file mode 100644 index 000000000..80416ba65 --- /dev/null +++ b/apps/web-ele/src/router/routes/modules/mall.ts @@ -0,0 +1,76 @@ +import type { RouteRecordRaw } from 'vue-router'; + +const routes: RouteRecordRaw[] = [ + { + path: '/mall/product', + name: 'ProductCenter', + meta: { + title: '商品中心', + icon: 'lucide:shopping-bag', + keepAlive: true, + hideInMenu: true, + }, + children: [ + { + path: 'spu/add', + name: 'ProductSpuAdd', + meta: { + title: '商品添加', + activeMenu: '/mall/product/spu', + }, + component: () => import('#/views/mall/product/spu/modules/form.vue'), + }, + { + path: String.raw`spu/edit/:id(\d+)`, + name: 'ProductSpuEdit', + meta: { + title: '商品编辑', + activeMenu: '/mall/product/spu', + }, + component: () => import('#/views/mall/product/spu/modules/form.vue'), + }, + { + path: String.raw`spu/detail/:id(\d+)`, + name: 'ProductSpuDetail', + meta: { + title: '商品详情', + activeMenu: '/crm/business', + }, + component: () => import('#/views/mall/product/spu/modules/detail.vue'), + }, + ], + }, + // { + // path: '/mall/trade', + // name: 'TradeCenter', + // meta: { + // title: '交易中心', + // icon: 'lucide:shopping-cart', + // keepAlive: true, + // hideInMenu: true, + // }, + // children: [ + // { + // path: String.raw`order/detail/:id(\d+)`, + // name: 'TradeOrderDetail', + // meta: { + // title: '订单详情', + // activeMenu: '/mall/trade/order', + // }, + // component: () => import('#/views/mall/trade/order/detail/index.vue'), + // }, + // { + // path: String.raw`after-sale/detail/:id(\d+)`, + // name: 'TradeAfterSaleDetail', + // meta: { + // title: '退款详情', + // activeMenu: '/mall/trade/after-sale', + // }, + // component: () => + // import('#/views/mall/trade/afterSale/detail/index.vue'), + // }, + // ], + // }, +]; + +export default routes; diff --git a/apps/web-ele/src/utils/bean.ts b/apps/web-ele/src/utils/bean.ts new file mode 100644 index 000000000..fa4e3c028 --- /dev/null +++ b/apps/web-ele/src/utils/bean.ts @@ -0,0 +1,17 @@ +/** + * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2} + * @param target 目标对象 + * @param source 源对象 + */ +export const copyValueToTarget = (target: any, source: any) => { + const newObj = Object.assign({}, target, source); + // 删除多余属性 + Object.keys(newObj).forEach((key) => { + // 如果不是target中的属性则删除 + if (Object.keys(target).indexOf(key) === -1) { + delete newObj[key]; + } + }); + // 更新目标对象值 + Object.assign(target, newObj); +}; diff --git a/apps/web-ele/src/utils/formatNum.ts b/apps/web-ele/src/utils/formatNum.ts new file mode 100644 index 000000000..fd8fbb1e3 --- /dev/null +++ b/apps/web-ele/src/utils/formatNum.ts @@ -0,0 +1,38 @@ +/** + * 将一个整数转换为分数保留两位小数 + * @param num + */ +export const formatToFraction = (num: number | string | undefined): string => { + if (typeof num === 'undefined') return '0.00'; + const parsedNumber = typeof num === 'string' ? parseFloat(num) : num; + return (parsedNumber / 100.0).toFixed(2); +}; + +/** + * 将一个数转换为 1.00 这样 + * 数据呈现的时候使用 + * + * @param num 整数 + */ +// TODO @芋艿:看看怎么融合掉 +export const floatToFixed2 = (num: number | string | undefined): string => { + let str = '0.00'; + if (typeof num === 'undefined') { + return str; + } + const f = formatToFraction(num); + const decimalPart = f.toString().split('.')[1]; + const len = decimalPart ? decimalPart.length : 0; + switch (len) { + case 0: + str = f.toString() + '.00'; + break; + case 1: + str = f.toString() + '0'; + break; + case 2: + str = f.toString(); + break; + } + return str; +}; diff --git a/apps/web-ele/src/utils/index.ts b/apps/web-ele/src/utils/index.ts index 022e6441d..f07b644bd 100644 --- a/apps/web-ele/src/utils/index.ts +++ b/apps/web-ele/src/utils/index.ts @@ -5,3 +5,7 @@ export * from './formCreate'; export * from './rangePickerProps'; export * from './routerHelper'; export * from './validator'; +export * from './tree'; +export * from './formatNum'; +export * from './is'; +export * from './bean'; diff --git a/apps/web-ele/src/utils/is.ts b/apps/web-ele/src/utils/is.ts new file mode 100644 index 000000000..cd2dcc376 --- /dev/null +++ b/apps/web-ele/src/utils/is.ts @@ -0,0 +1,117 @@ +// copy to vben-admin + +const toString = Object.prototype.toString + +export const is = (val: unknown, type: string) => { + return toString.call(val) === `[object ${type}]` +} + +export const isDef = (val?: T): val is T => { + return typeof val !== 'undefined' +} + +export const isUnDef = (val?: T): val is T => { + return !isDef(val) +} + +export const isObject = (val: any): val is Record => { + return val !== null && is(val, 'Object') +} + +export const isEmpty = (val: any): boolean => { + if (val === null || val === undefined || typeof val === 'undefined') { + return true + } + if (isArray(val) || isString(val)) { + return val.length === 0 + } + + if (val instanceof Map || val instanceof Set) { + return val.size === 0 + } + + if (isObject(val)) { + return Object.keys(val).length === 0 + } + + return false +} + +export const isDate = (val: unknown): val is Date => { + return is(val, 'Date') +} + +export const isNull = (val: unknown): val is null => { + return val === null +} + +export const isNullAndUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) && isNull(val) +} + +export const isNullOrUnDef = (val: unknown): val is null | undefined => { + return isUnDef(val) || isNull(val) +} + +export const isNumber = (val: unknown): val is number => { + return is(val, 'Number') +} + +export const isPromise = (val: unknown): val is Promise => { + return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) +} + +export const isString = (val: unknown): val is string => { + return is(val, 'String') +} + +export const isFunction = (val: unknown): val is Function => { + return typeof val === 'function' +} + +export const isBoolean = (val: unknown): val is boolean => { + return is(val, 'Boolean') +} + +export const isRegExp = (val: unknown): val is RegExp => { + return is(val, 'RegExp') +} + +export const isArray = (val: any): val is Array => { + return val && Array.isArray(val) +} + +export const isWindow = (val: any): val is Window => { + return typeof window !== 'undefined' && is(val, 'Window') +} + +export const isElement = (val: unknown): val is Element => { + return isObject(val) && !!val.tagName +} + +export const isMap = (val: unknown): val is Map => { + return is(val, 'Map') +} + +export const isServer = typeof window === 'undefined' + +export const isClient = !isServer + +export const isUrl = (path: string): boolean => { + const reg = + /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/ + return reg.test(path) +} + +export const isDark = (): boolean => { + return window.matchMedia('(prefers-color-scheme: dark)').matches +} + +// 是否是图片链接 +export const isImgPath = (path: string): boolean => { + return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) +} + +export const isEmptyVal = (val: any): boolean => { + return val === '' || val === null || val === undefined +} diff --git a/apps/web-ele/src/utils/tree.ts b/apps/web-ele/src/utils/tree.ts new file mode 100644 index 000000000..1d6adf2a8 --- /dev/null +++ b/apps/web-ele/src/utils/tree.ts @@ -0,0 +1,440 @@ +interface TreeHelperConfig { + id: string; + children: string; + pid: string; +} + +const DEFAULT_CONFIG: TreeHelperConfig = { + id: 'id', + children: 'children', + pid: 'pid', +}; +export const defaultProps = { + children: 'children', + label: 'name', + value: 'id', + isLeaf: 'leaf', + emitPath: false, // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 +}; +interface Fn { + (...arg: T[]): T; +} + +const getConfig = (config: Partial) => + Object.assign({}, DEFAULT_CONFIG, config); + +// tree from list +export const listToTree = ( + list: any[], + config: Partial = {}, +): T[] => { + const conf = getConfig(config) as TreeHelperConfig; + const nodeMap = new Map(); + const result: T[] = []; + const { id, children, pid } = conf; + + for (const node of list) { + node[children] = node[children] || []; + nodeMap.set(node[id], node); + } + for (const node of list) { + const parent = nodeMap.get(node[pid]); + (parent ? parent.children : result).push(node); + } + return result; +}; + +export const treeToList = ( + tree: any, + config: Partial = {}, +): T => { + config = getConfig(config); + const { children } = config; + const result: any = [...tree]; + for (let i = 0; i < result.length; i++) { + if (!result[i][children!]) continue; + result.splice(i + 1, 0, ...result[i][children!]); + } + return result; +}; + +export const findNode = ( + tree: any, + func: Fn, + config: Partial = {}, +): T | null => { + config = getConfig(config); + const { children } = config; + const list = [...tree]; + for (const node of list) { + if (func(node)) return node; + node[children!] && list.push(...node[children!]); + } + return null; +}; + +export const findNodeAll = ( + tree: any, + func: Fn, + config: Partial = {}, +): T[] => { + config = getConfig(config); + const { children } = config; + const list = [...tree]; + const result: T[] = []; + for (const node of list) { + func(node) && result.push(node); + node[children!] && list.push(...node[children!]); + } + return result; +}; + +export const findPath = ( + tree: any, + func: Fn, + config: Partial = {}, +): T | T[] | null => { + config = getConfig(config); + const path: T[] = []; + const list = [...tree]; + const visitedSet = new Set(); + const { children } = config; + while (list.length) { + const node = list[0]; + if (visitedSet.has(node)) { + path.pop(); + list.shift(); + } else { + visitedSet.add(node); + node[children!] && list.unshift(...node[children!]); + path.push(node); + if (func(node)) { + return path; + } + } + } + return null; +}; + +export const findPathAll = ( + tree: any, + func: Fn, + config: Partial = {}, +) => { + config = getConfig(config); + const path: any[] = []; + const list = [...tree]; + const result: any[] = []; + const visitedSet = new Set(), + { children } = config; + while (list.length) { + const node = list[0]; + if (visitedSet.has(node)) { + path.pop(); + list.shift(); + } else { + visitedSet.add(node); + node[children!] && list.unshift(...node[children!]); + path.push(node); + func(node) && result.push([...path]); + } + } + return result; +}; + +export const filter = ( + tree: T[], + func: (n: T) => boolean, + config: Partial = {}, +): T[] => { + config = getConfig(config); + const children = config.children as string; + + function listFilter(list: T[]) { + return list + .map((node: any) => ({ ...node })) + .filter((node) => { + node[children] = node[children] && listFilter(node[children]); + return func(node) || (node[children] && node[children].length); + }); + } + + return listFilter(tree); +}; + +export const forEach = ( + tree: T[], + func: (n: T) => any, + config: Partial = {}, +): void => { + config = getConfig(config); + const list: any[] = [...tree]; + const { children } = config; + for (let i = 0; i < list.length; i++) { + // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿 + if (func(list[i])) { + return; + } + children && + list[i][children] && + list.splice(i + 1, 0, ...list[i][children]); + } +}; + +/** + * @description: Extract tree specified structure + */ +export const treeMap = ( + treeData: T[], + opt: { children?: string; conversion: Fn }, +): T[] => { + return treeData.map((item) => treeMapEach(item, opt)); +}; + +/** + * @description: Extract tree specified structure + */ +export const treeMapEach = ( + data: any, + { children = 'children', conversion }: { children?: string; conversion: Fn }, +) => { + const haveChildren = + Array.isArray(data[children]) && data[children].length > 0; + const conversionData = conversion(data) || {}; + if (haveChildren) { + return { + ...conversionData, + [children]: data[children].map((i: number) => + treeMapEach(i, { + children, + conversion, + }), + ), + }; + } else { + return { + ...conversionData, + }; + } +}; + +/** + * 递归遍历树结构 + * @param treeDatas 树 + * @param callBack 回调 + * @param parentNode 父节点 + */ +export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => { + treeDatas.forEach((element) => { + const newNode = callBack(element, parentNode) || element; + if (element.children) { + eachTree(element.children, callBack, newNode); + } + }); +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + */ +export const handleTree = ( + data: any[], + id?: string, + parentId?: string, + children?: string, +) => { + if (!Array.isArray(data)) { + console.warn('data must be an array'); + return []; + } + const config = { + id: id || 'id', + parentId: parentId || 'parentId', + childrenList: children || 'children', + }; + + const childrenListMap: Record = {}; + const nodeIds: Record = {}; + const tree: any[] = []; + + for (const d of data) { + const parentId = d[config.parentId]; + if (childrenListMap[parentId] == null) { + childrenListMap[parentId] = []; + } + nodeIds[d[config.id]] = d; + childrenListMap[parentId].push(d); + } + + for (const d of data) { + const parentId = d[config.parentId]; + if (nodeIds[parentId] == null) { + tree.push(d); + } + } + + for (const t of tree) { + adaptToChildrenList(t); + } + + function adaptToChildrenList(o: any) { + if (childrenListMap[o[config.id]] !== null) { + o[config.childrenList] = childrenListMap[o[config.id]]; + } + if (o[config.childrenList]) { + for (const c of o[config.childrenList]) { + adaptToChildrenList(c); + } + } + } + + return tree; +}; + +/** + * 构造树型结构数据 + * @param {*} data 数据源 + * @param {*} id id字段 默认 'id' + * @param {*} parentId 父节点字段 默认 'parentId' + * @param {*} children 孩子节点字段 默认 'children' + * @param {*} rootId 根Id 默认 0 + */ +// @ts-ignore +export const handleTree2 = (data, id, parentId, children, rootId) => { + id = id || 'id'; + parentId = parentId || 'parentId'; + // children = children || 'children' + rootId = + rootId || + Math.min( + ...data.map((item: any) => { + return item[parentId]; + }), + ) || + 0; + // 对源数据深度克隆 + const cloneData = JSON.parse(JSON.stringify(data)); + // 循环所有项 + const treeData = cloneData.filter((father: any) => { + const branchArr = cloneData.filter((child: any) => { + // 返回每一项的子级数组 + return father[id] === child[parentId]; + }); + branchArr.length > 0 ? (father.children = branchArr) : ''; + // 返回第一层 + return father[parentId] === rootId; + }); + return treeData !== '' ? treeData : data; +}; + +/** + * 校验选中的节点,是否为指定 level + * + * @param tree 要操作的树结构数据 + * @param nodeId 需要判断在什么层级的数据 + * @param level 检查的级别, 默认检查到二级 + * @return true 是;false 否 + */ +export const checkSelectedNode = ( + tree: any[], + nodeId: any, + level = 2, +): boolean => { + if ( + typeof tree === 'undefined' || + !Array.isArray(tree) || + tree.length === 0 + ) { + console.warn('tree must be an array'); + return false; + } + + // 校验是否是一级节点 + if (tree.some((item) => item.id === nodeId)) { + return false; + } + + // 递归计数 + let count = 1; + + // 深层次校验 + function performAThoroughValidation(arr: any[]): boolean { + count += 1; + for (const item of arr) { + if (item.id === nodeId) { + return true; + } else if ( + typeof item.children !== 'undefined' && + item.children.length !== 0 + ) { + if (performAThoroughValidation(item.children)) { + return true; + } + } + } + return false; + } + + for (const item of tree) { + count = 1; + if (performAThoroughValidation(item.children)) { + // 找到后对比是否是期望的层级 + if (count >= level) { + return true; + } + } + } + + return false; +}; + +/** + * 获取节点的完整结构 + * @param tree 树数据 + * @param nodeId 节点 id + */ +export const treeToString = (tree: any[], nodeId: any) => { + if ( + typeof tree === 'undefined' || + !Array.isArray(tree) || + tree.length === 0 + ) { + console.warn('tree must be an array'); + return ''; + } + // 校验是否是一级节点 + const node = tree.find((item) => item.id === nodeId); + if (typeof node !== 'undefined') { + return node.name; + } + let str = ''; + + function performAThoroughValidation(arr: any[]) { + for (const item of arr) { + if (item.id === nodeId) { + str += ` / ${item.name}`; + return true; + } else if ( + typeof item.children !== 'undefined' && + item.children.length !== 0 + ) { + str += ` / ${item.name}`; + if (performAThoroughValidation(item.children)) { + return true; + } + } + } + return false; + } + + for (const item of tree) { + str = `${item.name}`; + if (performAThoroughValidation(item.children)) { + break; + } + } + return str; +}; diff --git a/apps/web-ele/src/views/mall/product/brand/data.ts b/apps/web-ele/src/views/mall/product/brand/data.ts index 654809ac9..ac0271e67 100644 --- a/apps/web-ele/src/views/mall/product/brand/data.ts +++ b/apps/web-ele/src/views/mall/product/brand/data.ts @@ -103,6 +103,9 @@ export function useGridColumns(): VxeGridPropTypes.Columns { title: '品牌图片', cellRender: { name: 'CellImage', + props: { + class: 'w-10 h-10', + }, }, }, { diff --git a/apps/web-ele/src/views/mall/product/spu/components/delivery-form.vue b/apps/web-ele/src/views/mall/product/spu/components/delivery-form.vue new file mode 100644 index 000000000..c99de0f77 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/delivery-form.vue @@ -0,0 +1,84 @@ + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/description-form.vue b/apps/web-ele/src/views/mall/product/spu/components/description-form.vue new file mode 100644 index 000000000..53b1c0741 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/description-form.vue @@ -0,0 +1,66 @@ + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/info-form.vue b/apps/web-ele/src/views/mall/product/spu/components/info-form.vue new file mode 100644 index 000000000..c5bd3d446 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/info-form.vue @@ -0,0 +1,138 @@ + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/model.d.ts b/apps/web-ele/src/views/mall/product/spu/components/model.d.ts new file mode 100644 index 000000000..01e5ef181 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/model.d.ts @@ -0,0 +1,62 @@ +import SkuList from './SkuList.vue'; +import { Spu } from '@/api/mall/product/spu'; + +interface PropertyAndValues { + id: number; + name: string; + values?: PropertyAndValues[]; +} + +interface RuleConfig { + // 需要校验的字段 + // 例:name: 'name' 则表示校验 sku.name 的值 + // 例:name: 'productConfig.stock' 则表示校验 sku.productConfig.name 的值,此处 productConfig 表示我在 Sku 上扩展的属性 + name: string; + // 校验规格为一个毁掉函数,其中 arg 为需要校验的字段的值。 + // 例:需要校验价格必须大于0.01 + // { + // name:'price', + // rule:(arg: number) => arg > 0.01 + // } + rule: (arg: any) => boolean; + // 校验不通过时的消息提示 + message: string; +} + +/** + * 获得商品的规格列表 - 商品相关的公共函数 + * + * @param spu + * @return PropertyAndValues 规格列表 + */ +const getPropertyList = (spu: Spu): PropertyAndValues[] => { + // 直接拿返回的 skus 属性逆向生成出 propertyList + const properties: PropertyAndValues[] = []; + // 只有是多规格才处理 + if (spu.specType) { + spu.skus?.forEach((sku) => { + sku.properties?.forEach( + ({ propertyId, propertyName, valueId, valueName }) => { + // 添加属性 + if (!properties?.some((item) => item.id === propertyId)) { + properties.push({ + id: propertyId!, + name: propertyName!, + values: [], + }); + } + // 添加属性值 + const index = properties?.findIndex((item) => item.id === propertyId); + if ( + !properties[index].values?.some((value) => value.id === valueId) + ) { + properties[index].values?.push({ id: valueId!, name: valueName! }); + } + }, + ); + }); + } + return properties; +}; + +export { SkuList, PropertyAndValues, RuleConfig, getPropertyList }; diff --git a/apps/web-ele/src/views/mall/product/spu/components/other-form.vue b/apps/web-ele/src/views/mall/product/spu/components/other-form.vue new file mode 100644 index 000000000..dcb9e1bd5 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/other-form.vue @@ -0,0 +1,86 @@ + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue b/apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue new file mode 100644 index 000000000..5a488b584 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue @@ -0,0 +1,195 @@ + + + + + 属性名: + + {{ item.name }} + + + + 属性值: + + {{ value.name }} + + + + + + + 添加 + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/product-property-add-form.vue b/apps/web-ele/src/views/mall/product/spu/components/product-property-add-form.vue new file mode 100644 index 000000000..e74826645 --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/product-property-add-form.vue @@ -0,0 +1,130 @@ + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/sku-form.vue b/apps/web-ele/src/views/mall/product/spu/components/sku-form.vue new file mode 100644 index 000000000..8032c619e --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/sku-form.vue @@ -0,0 +1,192 @@ + + + + + + + + + 添加属性 + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue b/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue new file mode 100644 index 000000000..96ce2770c --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue @@ -0,0 +1,613 @@ + + + + + + + + + + + + + + {{ row.properties?.[index]?.valueName }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 批量添加 + + 删除 + + + + + + + + + + + + + + + + + + {{ row.properties?.[index]?.valueName }} + + + + + + + {{ row.barCode }} + + + + + {{ row.price }} + + + + + {{ row.marketPrice }} + + + + + {{ row.costPrice }} + + + + + {{ row.stock }} + + + + + {{ row.weight }} + + + + + {{ row.volume }} + + + + + + {{ row.firstBrokeragePrice }} + + + + + {{ row.secondBrokeragePrice }} + + + + + + + + + + + + + + + + + + + {{ row.properties?.[index]?.valueName }} + + + + + + + {{ row.barCode }} + + + + + {{ formatToFraction(row.price) }} + + + + + {{ formatToFraction(row.marketPrice) }} + + + + + {{ formatToFraction(row.costPrice) }} + + + + + {{ row.stock }} + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/modules/form.vue b/apps/web-ele/src/views/mall/product/spu/modules/form.vue index 5b6413575..9011d2c5e 100644 --- a/apps/web-ele/src/views/mall/product/spu/modules/form.vue +++ b/apps/web-ele/src/views/mall/product/spu/modules/form.vue @@ -1,3 +1,127 @@ - + + + + + + + + + + + + + + + + + + + + + + + -- Gitee From 4fafb39efc206ced09d4728d47769f093adc5102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=83=E8=B4=A7?= <252048765@qq.com> Date: Mon, 7 Jul 2025 07:25:38 +0800 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=95=86?= =?UTF-8?q?=E5=93=81=E5=B1=9E=E6=80=A7=E7=AE=A1=E7=90=86=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E5=8C=85=E5=90=AB=E5=B1=9E=E6=80=A7=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=92=8C=E5=B1=9E=E6=80=A7=E5=80=BC=E7=9A=84=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/mall/product/spu/components/data.ts | 42 ++++++ .../mall/product/spu/components/model.d.ts | 37 ----- ...-fttributes.vue => product-attributes.vue} | 129 +++++++++--------- .../mall/product/spu/components/sku-form.vue | 63 ++++++++- 4 files changed, 164 insertions(+), 107 deletions(-) create mode 100644 apps/web-ele/src/views/mall/product/spu/components/data.ts rename apps/web-ele/src/views/mall/product/spu/components/{product-fttributes.vue => product-attributes.vue} (64%) diff --git a/apps/web-ele/src/views/mall/product/spu/components/data.ts b/apps/web-ele/src/views/mall/product/spu/components/data.ts new file mode 100644 index 000000000..50463a42d --- /dev/null +++ b/apps/web-ele/src/views/mall/product/spu/components/data.ts @@ -0,0 +1,42 @@ +import type { MallSpuApi } from '#/api/mall/product/spu'; +import type { PropertyAndValues } from './model'; + +/** + * 获得商品的规格列表 - 商品相关的公共函数 + * + * @param spu + * @return PropertyAndValues 规格列表 + */ +const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => { + // 直接拿返回的 skus 属性逆向生成出 propertyList + const properties: PropertyAndValues[] = []; + // 只有是多规格才处理 + if (spu.specType) { + spu.skus?.forEach((sku) => { + sku.properties?.forEach( + ({ propertyId, propertyName, valueId, valueName }) => { + // 添加属性 + if (!properties?.some((item) => item.id === propertyId)) { + properties.push({ + id: propertyId!, + name: propertyName!, + values: [], + }); + } + // 添加属性值 + const index = properties?.findIndex((item) => item.id === propertyId); + if ( + index !== undefined && + index >= 0 && + !properties[index]!.values?.some((value) => value.id === valueId) + ) { + properties[index]!.values?.push({ id: valueId!, name: valueName! }); + } + }, + ); + }); + } + return properties; +}; + +export { getPropertyList }; diff --git a/apps/web-ele/src/views/mall/product/spu/components/model.d.ts b/apps/web-ele/src/views/mall/product/spu/components/model.d.ts index 01e5ef181..ad01b21fa 100644 --- a/apps/web-ele/src/views/mall/product/spu/components/model.d.ts +++ b/apps/web-ele/src/views/mall/product/spu/components/model.d.ts @@ -1,5 +1,4 @@ import SkuList from './SkuList.vue'; -import { Spu } from '@/api/mall/product/spu'; interface PropertyAndValues { id: number; @@ -23,40 +22,4 @@ interface RuleConfig { message: string; } -/** - * 获得商品的规格列表 - 商品相关的公共函数 - * - * @param spu - * @return PropertyAndValues 规格列表 - */ -const getPropertyList = (spu: Spu): PropertyAndValues[] => { - // 直接拿返回的 skus 属性逆向生成出 propertyList - const properties: PropertyAndValues[] = []; - // 只有是多规格才处理 - if (spu.specType) { - spu.skus?.forEach((sku) => { - sku.properties?.forEach( - ({ propertyId, propertyName, valueId, valueName }) => { - // 添加属性 - if (!properties?.some((item) => item.id === propertyId)) { - properties.push({ - id: propertyId!, - name: propertyName!, - values: [], - }); - } - // 添加属性值 - const index = properties?.findIndex((item) => item.id === propertyId); - if ( - !properties[index].values?.some((value) => value.id === valueId) - ) { - properties[index].values?.push({ id: valueId!, name: valueName! }); - } - }, - ); - }); - } - return properties; -}; - export { SkuList, PropertyAndValues, RuleConfig, getPropertyList }; diff --git a/apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue b/apps/web-ele/src/views/mall/product/spu/components/product-attributes.vue similarity index 64% rename from apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue rename to apps/web-ele/src/views/mall/product/spu/components/product-attributes.vue index 5a488b584..f1c61d3e4 100644 --- a/apps/web-ele/src/views/mall/product/spu/components/product-fttributes.vue +++ b/apps/web-ele/src/views/mall/product/spu/components/product-attributes.vue @@ -1,55 +1,57 @@ - + - 属性名: - + 属性名: + {{ item.name }} - + - 属性值: - - {{ value.name }} - - - - - - + 添加 - + + 属性值: + + {{ value.name }} + + + + + + + 添加 + + - - + + @@ -178,12 +213,28 @@ const generateSkus = (propertyList: any[]) => { /> - 添加属性 - + 添加属性 + + + + + + + + -- Gitee From 3326c25a0d9016d0ea04f1273c04fc078899da16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=83=E8=B4=A7?= <252048765@qq.com> Date: Mon, 7 Jul 2025 07:38:43 +0800 Subject: [PATCH 4/6] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=B7=A5=E5=85=B7=E5=87=BD=E6=95=B0=EF=BC=8C?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20SKU=20=E5=88=97=E8=A1=A8=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E9=80=BB=E8=BE=91=E5=92=8C=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web-ele/src/utils/formatNum.ts | 38 -------- .../mall/product/spu/components/sku-list.vue | 93 +------------------ .../views/mall/product/spu/modules/form.vue | 85 +++++++++++++++-- 3 files changed, 83 insertions(+), 133 deletions(-) delete mode 100644 apps/web-ele/src/utils/formatNum.ts diff --git a/apps/web-ele/src/utils/formatNum.ts b/apps/web-ele/src/utils/formatNum.ts deleted file mode 100644 index fd8fbb1e3..000000000 --- a/apps/web-ele/src/utils/formatNum.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * 将一个整数转换为分数保留两位小数 - * @param num - */ -export const formatToFraction = (num: number | string | undefined): string => { - if (typeof num === 'undefined') return '0.00'; - const parsedNumber = typeof num === 'string' ? parseFloat(num) : num; - return (parsedNumber / 100.0).toFixed(2); -}; - -/** - * 将一个数转换为 1.00 这样 - * 数据呈现的时候使用 - * - * @param num 整数 - */ -// TODO @芋艿:看看怎么融合掉 -export const floatToFixed2 = (num: number | string | undefined): string => { - let str = '0.00'; - if (typeof num === 'undefined') { - return str; - } - const f = formatToFraction(num); - const decimalPart = f.toString().split('.')[1]; - const len = decimalPart ? decimalPart.length : 0; - switch (len) { - case 0: - str = f.toString() + '.00'; - break; - case 1: - str = f.toString() + '0'; - break; - case 2: - str = f.toString(); - break; - } - return str; -}; diff --git a/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue b/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue index 96ce2770c..1dcf4a532 100644 --- a/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue +++ b/apps/web-ele/src/views/mall/product/spu/components/sku-list.vue @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - {{ row.properties?.[index]?.valueName }} - - - - - - - {{ row.barCode }} - - - - - {{ row.price }} - - - - - {{ row.marketPrice }} - - - - - {{ row.costPrice }} - - - - - {{ row.stock }} - - - - - {{ row.weight }} - - - - - {{ row.volume }} - - - - - - {{ row.firstBrokeragePrice }} - - - - - {{ row.secondBrokeragePrice }} - - - - - - + + + + + + + 商品详情 + + + + + + + 编辑商品 + + + + 返回列表 + + + + + + + + + {{ formData.name }} + + {{ formData.introduction || '暂无简介' }} + + + 多规格 + 单规格 + 分销 + 库存: + {{ + formData.skus?.reduce( + (sum, sku) => sum + (sku.stock || 0), + 0, + ) || 0 + }} + 分类: {{ getCategoryNameById(formData.categoryId) }} + + + + + + + + + + + {{ + formData.name + }} + + {{ + getCategoryNameById(formData.categoryId) + }} + + + {{ + getBrandNameById(formData.brandId) + }} + + + {{ formData.keyword || '无' }} + {{ + formData.giveIntegral + }} + {{ + formData.virtualSalesCount + }} + {{ + formData.sort + }} + + + {{ formData.specType ? '多规格' : '单规格' }} + + + + + {{ formData.subCommissionType ? '单独设置' : '默认设置' }} + + + + + + + + + + + + {{ getDeliveryTypeName(type) }} + + + 暂无配送方式 + + + + {{ + formData.deliveryTemplateId || '未设置' + }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 价格信息 + + + 销售价: + + ¥{{ sku.price }} + + 市场价: + ¥{{ sku.marketPrice }} + 成本价: + ¥{{ sku.costPrice }} + + + + + + + 库存信息 + + + 库存: + {{ sku.stock }} 件 + 条码: + {{ sku.barCode || '未设置' }} + + + + + + + 物流信息 + + + 重量: + {{ sku.weight }} kg + 体积: + {{ sku.volume }} m³ + + + + + + + + + 分销佣金 + + + 一级佣金: + ¥{{ sku.firstBrokeragePrice }} + 二级佣金: + ¥{{ sku.secondBrokeragePrice }} + + + + + + 规格属性 + + + {{ prop.propertyName }}: {{ prop.valueName }} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web-ele/src/views/mall/product/spu/modules/form.vue b/apps/web-ele/src/views/mall/product/spu/modules/form.vue index 9df121763..a1619905a 100644 --- a/apps/web-ele/src/views/mall/product/spu/modules/form.vue +++ b/apps/web-ele/src/views/mall/product/spu/modules/form.vue @@ -4,7 +4,7 @@ import { onMounted, ref, unref } from 'vue'; import { cloneDeep } from '@vben/utils'; import type { MallSpuApi } from '#/api/mall/product/spu'; import { useRouter, useRoute } from 'vue-router'; -import { floatToFixed2, formatToFraction, convertToInteger } from '@vben/utils'; +import { formatToFraction, convertToInteger } from '@vben/utils'; import * as ProductSpuApi from '#/api/mall/product/spu'; import { ElMessage } from 'element-plus'; @@ -51,36 +51,22 @@ const formData = ref({ }); const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const isDetail = ref(false); // 是否查看详情 const { push } = useRouter(); // 路由 -const { params, name } = useRoute(); // 查询参数 +const { params } = useRoute(); // 查询参数 /** 获得详情 */ const getDetail = async () => { - if ('ProductSpuDetail' === name) { - isDetail.value = true; - } const id = params.id as unknown as number; if (id) { formLoading.value = true; try { const res = (await ProductSpuApi.getSpu(id)) as MallSpuApi.Spu; res.skus?.forEach((item: MallSpuApi.Sku) => { - if (isDetail.value) { - item.price = floatToFixed2(item.price); - item.marketPrice = floatToFixed2(item.marketPrice); - item.costPrice = floatToFixed2(item.costPrice); - item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice); - item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice); - } else { - // 回显价格分转元 - item.price = formatToFraction(item.price); - item.marketPrice = formatToFraction(item.marketPrice); - item.costPrice = formatToFraction(item.costPrice); - item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice); - item.secondBrokeragePrice = formatToFraction( - item.secondBrokeragePrice, - ); - } + // 回显价格分转元 + item.price = formatToFraction(item.price); + item.marketPrice = formatToFraction(item.marketPrice); + item.costPrice = formatToFraction(item.costPrice); + item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice); + item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice); }); formData.value = res; } finally { -- Gitee From 8f8f3481ff610962aa6345ea03bc61e439f8f68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=83=E8=B4=A7?= <252048765@qq.com> Date: Sun, 13 Jul 2025 09:08:12 +0800 Subject: [PATCH 6/6] =?UTF-8?q?refactor(web-ele):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=9C=AA=E4=BD=BF=E7=94=A8=E7=9A=84=E5=B7=A5=E5=85=B7=E5=87=BD?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除了 utils/index.ts 中未使用的树处理和类型判断函数 - 更新了相关文件中的导入路径 - 优化了代码结构,提高了代码的可读性和维护性 --- apps/web-ele/src/utils/index.ts | 2 - apps/web-ele/src/utils/is.ts | 117 ----- apps/web-ele/src/utils/tree.ts | 440 ------------------ .../mall/product/spu/components/info-form.vue | 2 +- .../mall/product/spu/components/sku-list.vue | 3 +- .../src/components/json-viewer/style.scss | 2 +- .../src/components/resize/resize.vue | 2 +- .../src/widgets/theme-toggle/theme-button.vue | 4 +- 8 files changed, 6 insertions(+), 566 deletions(-) delete mode 100644 apps/web-ele/src/utils/is.ts delete mode 100644 apps/web-ele/src/utils/tree.ts diff --git a/apps/web-ele/src/utils/index.ts b/apps/web-ele/src/utils/index.ts index 162976e7a..b29690876 100644 --- a/apps/web-ele/src/utils/index.ts +++ b/apps/web-ele/src/utils/index.ts @@ -5,6 +5,4 @@ export * from './formCreate'; export * from './rangePickerProps'; export * from './routerHelper'; export * from './validator'; -export * from './tree'; -export * from './is'; export * from './bean'; diff --git a/apps/web-ele/src/utils/is.ts b/apps/web-ele/src/utils/is.ts deleted file mode 100644 index cd2dcc376..000000000 --- a/apps/web-ele/src/utils/is.ts +++ /dev/null @@ -1,117 +0,0 @@ -// copy to vben-admin - -const toString = Object.prototype.toString - -export const is = (val: unknown, type: string) => { - return toString.call(val) === `[object ${type}]` -} - -export const isDef = (val?: T): val is T => { - return typeof val !== 'undefined' -} - -export const isUnDef = (val?: T): val is T => { - return !isDef(val) -} - -export const isObject = (val: any): val is Record => { - return val !== null && is(val, 'Object') -} - -export const isEmpty = (val: any): boolean => { - if (val === null || val === undefined || typeof val === 'undefined') { - return true - } - if (isArray(val) || isString(val)) { - return val.length === 0 - } - - if (val instanceof Map || val instanceof Set) { - return val.size === 0 - } - - if (isObject(val)) { - return Object.keys(val).length === 0 - } - - return false -} - -export const isDate = (val: unknown): val is Date => { - return is(val, 'Date') -} - -export const isNull = (val: unknown): val is null => { - return val === null -} - -export const isNullAndUnDef = (val: unknown): val is null | undefined => { - return isUnDef(val) && isNull(val) -} - -export const isNullOrUnDef = (val: unknown): val is null | undefined => { - return isUnDef(val) || isNull(val) -} - -export const isNumber = (val: unknown): val is number => { - return is(val, 'Number') -} - -export const isPromise = (val: unknown): val is Promise => { - return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) -} - -export const isString = (val: unknown): val is string => { - return is(val, 'String') -} - -export const isFunction = (val: unknown): val is Function => { - return typeof val === 'function' -} - -export const isBoolean = (val: unknown): val is boolean => { - return is(val, 'Boolean') -} - -export const isRegExp = (val: unknown): val is RegExp => { - return is(val, 'RegExp') -} - -export const isArray = (val: any): val is Array => { - return val && Array.isArray(val) -} - -export const isWindow = (val: any): val is Window => { - return typeof window !== 'undefined' && is(val, 'Window') -} - -export const isElement = (val: unknown): val is Element => { - return isObject(val) && !!val.tagName -} - -export const isMap = (val: unknown): val is Map => { - return is(val, 'Map') -} - -export const isServer = typeof window === 'undefined' - -export const isClient = !isServer - -export const isUrl = (path: string): boolean => { - const reg = - /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/ - return reg.test(path) -} - -export const isDark = (): boolean => { - return window.matchMedia('(prefers-color-scheme: dark)').matches -} - -// 是否是图片链接 -export const isImgPath = (path: string): boolean => { - return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) -} - -export const isEmptyVal = (val: any): boolean => { - return val === '' || val === null || val === undefined -} diff --git a/apps/web-ele/src/utils/tree.ts b/apps/web-ele/src/utils/tree.ts deleted file mode 100644 index 1d6adf2a8..000000000 --- a/apps/web-ele/src/utils/tree.ts +++ /dev/null @@ -1,440 +0,0 @@ -interface TreeHelperConfig { - id: string; - children: string; - pid: string; -} - -const DEFAULT_CONFIG: TreeHelperConfig = { - id: 'id', - children: 'children', - pid: 'pid', -}; -export const defaultProps = { - children: 'children', - label: 'name', - value: 'id', - isLeaf: 'leaf', - emitPath: false, // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值 -}; -interface Fn { - (...arg: T[]): T; -} - -const getConfig = (config: Partial) => - Object.assign({}, DEFAULT_CONFIG, config); - -// tree from list -export const listToTree = ( - list: any[], - config: Partial = {}, -): T[] => { - const conf = getConfig(config) as TreeHelperConfig; - const nodeMap = new Map(); - const result: T[] = []; - const { id, children, pid } = conf; - - for (const node of list) { - node[children] = node[children] || []; - nodeMap.set(node[id], node); - } - for (const node of list) { - const parent = nodeMap.get(node[pid]); - (parent ? parent.children : result).push(node); - } - return result; -}; - -export const treeToList = ( - tree: any, - config: Partial = {}, -): T => { - config = getConfig(config); - const { children } = config; - const result: any = [...tree]; - for (let i = 0; i < result.length; i++) { - if (!result[i][children!]) continue; - result.splice(i + 1, 0, ...result[i][children!]); - } - return result; -}; - -export const findNode = ( - tree: any, - func: Fn, - config: Partial = {}, -): T | null => { - config = getConfig(config); - const { children } = config; - const list = [...tree]; - for (const node of list) { - if (func(node)) return node; - node[children!] && list.push(...node[children!]); - } - return null; -}; - -export const findNodeAll = ( - tree: any, - func: Fn, - config: Partial = {}, -): T[] => { - config = getConfig(config); - const { children } = config; - const list = [...tree]; - const result: T[] = []; - for (const node of list) { - func(node) && result.push(node); - node[children!] && list.push(...node[children!]); - } - return result; -}; - -export const findPath = ( - tree: any, - func: Fn, - config: Partial = {}, -): T | T[] | null => { - config = getConfig(config); - const path: T[] = []; - const list = [...tree]; - const visitedSet = new Set(); - const { children } = config; - while (list.length) { - const node = list[0]; - if (visitedSet.has(node)) { - path.pop(); - list.shift(); - } else { - visitedSet.add(node); - node[children!] && list.unshift(...node[children!]); - path.push(node); - if (func(node)) { - return path; - } - } - } - return null; -}; - -export const findPathAll = ( - tree: any, - func: Fn, - config: Partial = {}, -) => { - config = getConfig(config); - const path: any[] = []; - const list = [...tree]; - const result: any[] = []; - const visitedSet = new Set(), - { children } = config; - while (list.length) { - const node = list[0]; - if (visitedSet.has(node)) { - path.pop(); - list.shift(); - } else { - visitedSet.add(node); - node[children!] && list.unshift(...node[children!]); - path.push(node); - func(node) && result.push([...path]); - } - } - return result; -}; - -export const filter = ( - tree: T[], - func: (n: T) => boolean, - config: Partial = {}, -): T[] => { - config = getConfig(config); - const children = config.children as string; - - function listFilter(list: T[]) { - return list - .map((node: any) => ({ ...node })) - .filter((node) => { - node[children] = node[children] && listFilter(node[children]); - return func(node) || (node[children] && node[children].length); - }); - } - - return listFilter(tree); -}; - -export const forEach = ( - tree: T[], - func: (n: T) => any, - config: Partial = {}, -): void => { - config = getConfig(config); - const list: any[] = [...tree]; - const { children } = config; - for (let i = 0; i < list.length; i++) { - // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿 - if (func(list[i])) { - return; - } - children && - list[i][children] && - list.splice(i + 1, 0, ...list[i][children]); - } -}; - -/** - * @description: Extract tree specified structure - */ -export const treeMap = ( - treeData: T[], - opt: { children?: string; conversion: Fn }, -): T[] => { - return treeData.map((item) => treeMapEach(item, opt)); -}; - -/** - * @description: Extract tree specified structure - */ -export const treeMapEach = ( - data: any, - { children = 'children', conversion }: { children?: string; conversion: Fn }, -) => { - const haveChildren = - Array.isArray(data[children]) && data[children].length > 0; - const conversionData = conversion(data) || {}; - if (haveChildren) { - return { - ...conversionData, - [children]: data[children].map((i: number) => - treeMapEach(i, { - children, - conversion, - }), - ), - }; - } else { - return { - ...conversionData, - }; - } -}; - -/** - * 递归遍历树结构 - * @param treeDatas 树 - * @param callBack 回调 - * @param parentNode 父节点 - */ -export const eachTree = (treeDatas: any[], callBack: Fn, parentNode = {}) => { - treeDatas.forEach((element) => { - const newNode = callBack(element, parentNode) || element; - if (element.children) { - eachTree(element.children, callBack, newNode); - } - }); -}; - -/** - * 构造树型结构数据 - * @param {*} data 数据源 - * @param {*} id id字段 默认 'id' - * @param {*} parentId 父节点字段 默认 'parentId' - * @param {*} children 孩子节点字段 默认 'children' - */ -export const handleTree = ( - data: any[], - id?: string, - parentId?: string, - children?: string, -) => { - if (!Array.isArray(data)) { - console.warn('data must be an array'); - return []; - } - const config = { - id: id || 'id', - parentId: parentId || 'parentId', - childrenList: children || 'children', - }; - - const childrenListMap: Record = {}; - const nodeIds: Record = {}; - const tree: any[] = []; - - for (const d of data) { - const parentId = d[config.parentId]; - if (childrenListMap[parentId] == null) { - childrenListMap[parentId] = []; - } - nodeIds[d[config.id]] = d; - childrenListMap[parentId].push(d); - } - - for (const d of data) { - const parentId = d[config.parentId]; - if (nodeIds[parentId] == null) { - tree.push(d); - } - } - - for (const t of tree) { - adaptToChildrenList(t); - } - - function adaptToChildrenList(o: any) { - if (childrenListMap[o[config.id]] !== null) { - o[config.childrenList] = childrenListMap[o[config.id]]; - } - if (o[config.childrenList]) { - for (const c of o[config.childrenList]) { - adaptToChildrenList(c); - } - } - } - - return tree; -}; - -/** - * 构造树型结构数据 - * @param {*} data 数据源 - * @param {*} id id字段 默认 'id' - * @param {*} parentId 父节点字段 默认 'parentId' - * @param {*} children 孩子节点字段 默认 'children' - * @param {*} rootId 根Id 默认 0 - */ -// @ts-ignore -export const handleTree2 = (data, id, parentId, children, rootId) => { - id = id || 'id'; - parentId = parentId || 'parentId'; - // children = children || 'children' - rootId = - rootId || - Math.min( - ...data.map((item: any) => { - return item[parentId]; - }), - ) || - 0; - // 对源数据深度克隆 - const cloneData = JSON.parse(JSON.stringify(data)); - // 循环所有项 - const treeData = cloneData.filter((father: any) => { - const branchArr = cloneData.filter((child: any) => { - // 返回每一项的子级数组 - return father[id] === child[parentId]; - }); - branchArr.length > 0 ? (father.children = branchArr) : ''; - // 返回第一层 - return father[parentId] === rootId; - }); - return treeData !== '' ? treeData : data; -}; - -/** - * 校验选中的节点,是否为指定 level - * - * @param tree 要操作的树结构数据 - * @param nodeId 需要判断在什么层级的数据 - * @param level 检查的级别, 默认检查到二级 - * @return true 是;false 否 - */ -export const checkSelectedNode = ( - tree: any[], - nodeId: any, - level = 2, -): boolean => { - if ( - typeof tree === 'undefined' || - !Array.isArray(tree) || - tree.length === 0 - ) { - console.warn('tree must be an array'); - return false; - } - - // 校验是否是一级节点 - if (tree.some((item) => item.id === nodeId)) { - return false; - } - - // 递归计数 - let count = 1; - - // 深层次校验 - function performAThoroughValidation(arr: any[]): boolean { - count += 1; - for (const item of arr) { - if (item.id === nodeId) { - return true; - } else if ( - typeof item.children !== 'undefined' && - item.children.length !== 0 - ) { - if (performAThoroughValidation(item.children)) { - return true; - } - } - } - return false; - } - - for (const item of tree) { - count = 1; - if (performAThoroughValidation(item.children)) { - // 找到后对比是否是期望的层级 - if (count >= level) { - return true; - } - } - } - - return false; -}; - -/** - * 获取节点的完整结构 - * @param tree 树数据 - * @param nodeId 节点 id - */ -export const treeToString = (tree: any[], nodeId: any) => { - if ( - typeof tree === 'undefined' || - !Array.isArray(tree) || - tree.length === 0 - ) { - console.warn('tree must be an array'); - return ''; - } - // 校验是否是一级节点 - const node = tree.find((item) => item.id === nodeId); - if (typeof node !== 'undefined') { - return node.name; - } - let str = ''; - - function performAThoroughValidation(arr: any[]) { - for (const item of arr) { - if (item.id === nodeId) { - str += ` / ${item.name}`; - return true; - } else if ( - typeof item.children !== 'undefined' && - item.children.length !== 0 - ) { - str += ` / ${item.name}`; - if (performAThoroughValidation(item.children)) { - return true; - } - } - } - return false; - } - - for (const item of tree) { - str = `${item.name}`; - if (performAThoroughValidation(item.children)) { - break; - } - } - return str; -}; diff --git a/apps/web-ele/src/views/mall/product/spu/components/info-form.vue b/apps/web-ele/src/views/mall/product/spu/components/info-form.vue index c5bd3d446..815102024 100644 --- a/apps/web-ele/src/views/mall/product/spu/components/info-form.vue +++ b/apps/web-ele/src/views/mall/product/spu/components/info-form.vue @@ -1,6 +1,6 @@
+ 注意: + 积分活动涉及复杂的商品选择和SKU配置,当前为简化版本。 + 完整的商品选择和积分配置功能需要在后续版本中完善。 +
+ 说明: 当前为简化版本的满减送活动表单。 + 复杂的商品选择、优惠规则配置等功能已简化,仅保留基础字段配置。 + 如需完整功能,请参考原始 Element UI 版本的实现。 +