From 74551085e3d6c5751b6ecf67d078cecaee172451 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Thu, 9 Apr 2026 10:24:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=A7=92=E6=9D=80?= =?UTF-8?q?=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app/src/components/CardShop.vue | 263 +----- .../app/src/components/FlashSaleSection.vue | 435 +++++++++ packages/app/src/pages.json | 12 + packages/app/src/pages/admin/card-types.vue | 14 +- packages/app/src/pages/admin/flash-sales.vue | 863 ++++++++++++++++++ packages/app/src/pages/admin/index.vue | 16 + packages/app/src/pages/card/detail.vue | 270 +----- packages/app/src/pages/flash-sale/detail.vue | 837 +++++++++++++++++ packages/app/src/pages/home/index.vue | 8 +- packages/app/src/pages/profile/membership.vue | 580 ++++++------ packages/app/src/stores/admin.ts | 28 + packages/app/src/stores/flash-sale.ts | 43 + packages/app/src/utils/format.ts | 65 +- packages/server/prisma/schema.prisma | 65 +- packages/server/src/app.module.ts | 2 + .../flash-sale/dto/create-flash-sale.dto.ts | 35 + .../flash-sale/dto/update-flash-sale.dto.ts | 43 + .../flash-sale/flash-sale-admin.controller.ts | 67 ++ .../src/flash-sale/flash-sale.controller.ts | 40 + .../src/flash-sale/flash-sale.module.ts | 14 + .../src/flash-sale/flash-sale.service.ts | 409 +++++++++ packages/server/src/payment/payment.module.ts | 2 +- .../server/src/payment/payment.service.ts | 18 +- .../server/src/scheduler/scheduler.module.ts | 2 + .../server/src/scheduler/scheduler.service.ts | 19 +- packages/shared/src/enums.ts | 13 + packages/shared/src/index.ts | 11 + packages/shared/src/types/flash-sale.ts | 97 ++ packages/shared/src/types/index.ts | 10 + 29 files changed, 3521 insertions(+), 760 deletions(-) create mode 100644 packages/app/src/components/FlashSaleSection.vue create mode 100644 packages/app/src/pages/admin/flash-sales.vue create mode 100644 packages/app/src/pages/flash-sale/detail.vue create mode 100644 packages/app/src/stores/flash-sale.ts create mode 100644 packages/server/src/flash-sale/dto/create-flash-sale.dto.ts create mode 100644 packages/server/src/flash-sale/dto/update-flash-sale.dto.ts create mode 100644 packages/server/src/flash-sale/flash-sale-admin.controller.ts create mode 100644 packages/server/src/flash-sale/flash-sale.controller.ts create mode 100644 packages/server/src/flash-sale/flash-sale.module.ts create mode 100644 packages/server/src/flash-sale/flash-sale.service.ts create mode 100644 packages/shared/src/types/flash-sale.ts diff --git a/packages/app/src/components/CardShop.vue b/packages/app/src/components/CardShop.vue index de026b0..c7e134c 100644 --- a/packages/app/src/components/CardShop.vue +++ b/packages/app/src/components/CardShop.vue @@ -30,35 +30,10 @@ class="card-row" @tap="goToDetail(card.id)" > - + - - - - - - - - - - - - - - {{ getCardTypeLabel(card.type) }} - - {{ card.name }} - - ¥ - {{ formatPrice(card.price) }} - - - ¥{{ formatPrice(card.originalPrice) }} - - + + @@ -100,7 +75,7 @@ import { ref, onMounted } from 'vue' import type { CardType } from '@mp-pilates/shared' import { get } from '../utils/request' -import { formatPrice, getCardTypeLabel, getCardCoverClass } from '../utils/format' +import { formatPrice, getCardCoverClass } from '../utils/format' const cardTypes = ref([]) const loading = ref(false) @@ -180,249 +155,51 @@ function goToAllCards() { } /* ══════════════════════════════════════════════════════════ - CARD COVER — Horizontal premium card design + CARD COVER — Clean minimal design ══════════════════════════════════════════════════════════ */ .card-cover { - width: 240rpx; + width: 200rpx; height: 130rpx; border-radius: 16rpx; overflow: hidden; flex-shrink: 0; position: relative; - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 0; - - /* Glow effect behind */ - &::before { - content: ''; - position: absolute; - top: -20rpx; - left: -20rpx; - right: -20rpx; - bottom: -20rpx; - background: inherit; - filter: blur(24rpx) brightness(0.8); - z-index: 0; - opacity: 0.4; - } -} - -/* Left accent stripe */ -.cover-accent-bar { - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 6rpx; - background: rgba(255, 255, 255, 0.4); - z-index: 1; } /* Decorative circles */ .cover-deco { position: absolute; border-radius: 50%; - z-index: 0; pointer-events: none; - &--tl { - width: 60rpx; - height: 60rpx; - top: -16rpx; - right: 20rpx; - background: rgba(255, 255, 255, 0.1); + &--1 { + width: 100rpx; + height: 100rpx; + top: -30rpx; + right: -20rpx; + background: rgba(255, 255, 255, 0.4); } - &--br { - width: 80rpx; - height: 80rpx; - bottom: -24rpx; - left: -16rpx; - background: rgba(255, 255, 255, 0.07); - } -} - -/* CSS-drawn icons */ -.cover-icon { - width: 52rpx; - height: 52rpx; - position: relative; - z-index: 2; - flex-shrink: 0; - margin-left: 20rpx; -} - -/* 次卡 — stacked cards */ -.cover-icon--TIMES { - &::before { - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 36rpx; - height: 24rpx; - border: 2rpx solid rgba(255, 255, 255, 0.85); - border-radius: 5rpx; - box-sizing: border-box; - background: rgba(255, 255, 255, 0.12); - } - &::after { - content: ''; - position: absolute; - bottom: 10rpx; - left: 50%; - transform: translateX(-50%); - width: 36rpx; - height: 24rpx; - border: 2rpx solid rgba(255, 255, 255, 1); - border-radius: 5rpx; - box-sizing: border-box; - background: rgba(255, 255, 255, 0.2); - } -} - -/* 月卡 — calendar */ -.cover-icon--DURATION { - &::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 36rpx; - height: 30rpx; - border: 2rpx solid rgba(255, 255, 255, 0.9); - border-radius: 5rpx; - box-sizing: border-box; - } - &::after { - content: ''; - position: absolute; - top: 9rpx; - left: 50%; - transform: translateX(-50%); - width: 24rpx; - height: 0; - border-top: 2rpx solid rgba(255, 255, 255, 1); - box-shadow: - -6rpx 5rpx 0 0 rgba(255, 255, 255, 0.9), - 6rpx 5rpx 0 0 rgba(255, 255, 255, 0.9); - } -} - -/* 体验卡 — star */ -.cover-icon--TRIAL { - &::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 16rpx; - height: 16rpx; - border: 2rpx solid rgba(255, 255, 255, 1); - border-radius: 50%; - box-sizing: border-box; + &--2 { + width: 70rpx; + height: 70rpx; + bottom: -20rpx; + left: -10rpx; background: rgba(255, 255, 255, 0.25); } - &::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 2rpx; - height: 42rpx; - background: rgba(255, 255, 255, 0.8); - box-shadow: - 0 -12rpx 0 0 rgba(255, 255, 255, 0.8), - 0 12rpx 0 0 rgba(255, 255, 255, 0.8), - -12rpx 0 0 0 rgba(255, 255, 255, 0.8), - 12rpx 0 0 0 rgba(255, 255, 255, 0.8), - -8rpx -8rpx 0 0 rgba(255, 255, 255, 0.8), - 8rpx -8rpx 0 0 rgba(255, 255, 255, 0.8), - -8rpx 8rpx 0 0 rgba(255, 255, 255, 0.8), - 8rpx 8rpx 0 0 rgba(255, 255, 255, 0.8); - } } /* Card cover backgrounds */ .cover--times { - background: linear-gradient(135deg, #1e2340 0%, #2d2d5e 50%, #3a3a7a 100%); + background: linear-gradient(135deg, #E8D5C4 0%, #D4BFA8 100%); } .cover--duration { - background: linear-gradient(135deg, #4a1a6b 0%, #6c3483 50%, #8e4aaf 100%); + background: linear-gradient(135deg, #D8C8DC 0%, #C4AECB 100%); } .cover--trial { - background: linear-gradient(135deg, #14527a 0%, #1a6fa0 50%, #48a9a6 100%); -} - -/* Right side text content */ -.cover-content { - flex: 1; - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - padding: 0 16rpx 0 12rpx; - gap: 4rpx; - z-index: 2; -} - -.cover-badge { - padding: 3rpx 10rpx; - border-radius: 8rpx; - background: rgba(255, 255, 255, 0.18); - border: 1rpx solid rgba(255, 255, 255, 0.28); -} - -.cover-badge-text { - font-size: 16rpx; - color: rgba(255, 255, 255, 0.9); - font-weight: 600; -} - -.cover-name { - font-size: 24rpx; - font-weight: 700; - color: #ffffff; - letter-spacing: 0.5rpx; - line-height: 1.2; - max-width: 130rpx; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.cover-price-row { - display: flex; - align-items: baseline; - gap: 2rpx; -} - -.cover-currency { - font-size: 18rpx; - font-weight: 600; - color: rgba(255, 255, 255, 0.85); -} - -.cover-price { - font-size: 28rpx; - font-weight: 800; - color: #ffffff; - line-height: 1; -} - -.cover-original { - font-size: 16rpx; - color: rgba(255, 255, 255, 0.5); - text-decoration: line-through; + background: linear-gradient(135deg, #C8D8D2 0%, #A9C4BC 100%); } /* ── Card info — matches card-cover height ── */ diff --git a/packages/app/src/components/FlashSaleSection.vue b/packages/app/src/components/FlashSaleSection.vue new file mode 100644 index 0000000..9914d39 --- /dev/null +++ b/packages/app/src/components/FlashSaleSection.vue @@ -0,0 +1,435 @@ + + + + + diff --git a/packages/app/src/pages.json b/packages/app/src/pages.json index b345809..bbb2809 100644 --- a/packages/app/src/pages.json +++ b/packages/app/src/pages.json @@ -104,6 +104,18 @@ "style": { "navigationStyle": "custom" } + }, + { + "path": "pages/admin/flash-sales", + "style": { + "navigationStyle": "custom" + } + }, + { + "path": "pages/flash-sale/detail", + "style": { + "navigationStyle": "custom" + } } ], "globalStyle": { diff --git a/packages/app/src/pages/admin/card-types.vue b/packages/app/src/pages/admin/card-types.vue index 05216d8..ffddae5 100644 --- a/packages/app/src/pages/admin/card-types.vue +++ b/packages/app/src/pages/admin/card-types.vue @@ -522,16 +522,16 @@ onMounted(fetchCardTypes) justify-content: space-between; } -.header--times { background: linear-gradient(90deg, #1a1a2e, #2d2d5e); } -.header--duration { background: linear-gradient(90deg, #6c3483, #9b59b6); } -.header--trial { background: linear-gradient(90deg, #5a7a8a, $primary-dark); } +.header--times { background: linear-gradient(90deg, #E8D5C4, #D4BFA8); } +.header--duration { background: linear-gradient(90deg, #D8C8DC, #C4AECB); } +.header--trial { background: linear-gradient(90deg, #C8D8D2, #A9C4BC); } -.ct-type-label { font-size: 22rpx; font-weight: 600; color: #ffffff; letter-spacing: 2rpx; } +.ct-type-label { font-size: 22rpx; font-weight: 600; color: $brand-color; letter-spacing: 2rpx; } .ct-status-tag { border-radius: 20rpx; padding: 4rpx 16rpx; } -.tag--on { background: rgba(255,255,255,0.2); } -.tag--off { background: rgba(0,0,0,0.2); } -.ct-status-text { font-size: 20rpx; color: #ffffff; } +.tag--on { background: rgba(74, 64, 53, 0.1); } +.tag--off { background: rgba(74, 64, 53, 0.08); } +.ct-status-text { font-size: 20rpx; color: $brand-color; } .ct-body { padding: 24rpx; } diff --git a/packages/app/src/pages/admin/flash-sales.vue b/packages/app/src/pages/admin/flash-sales.vue new file mode 100644 index 0000000..ee4dc59 --- /dev/null +++ b/packages/app/src/pages/admin/flash-sales.vue @@ -0,0 +1,863 @@ +