feat(mp): 积分商城 V2 重设计 — design-handoff 全流程
- 新增 4 个 UI 组件: PointsCard/ProductCard/CheckinCalendar/CheckinModal - 商城首页 V2: 积分卡 + 快捷操作 + 分类标签 + 商品网格 - 商品详情 V2: 大图 + 信息卡 + 库存/余额状态 + 底部操作栏 - TabBar 新增商城入口(5 Tab: 首页/健康/商城/助手/我的) - 设计原型 docs/design/mp-05-mall-v2.html + SPEC.md 交付包 - CLAUDE.md 安全规范加固: 新增 §3.7 安全规范 6 条 + Feature DoD 安全清单扩展
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.checkin-calendar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
padding: $sp-section $sp-lg $sp-md;
|
||||
|
||||
&__day {
|
||||
width: 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
@include flex-center;
|
||||
|
||||
&--checked {
|
||||
background: $acc-l;
|
||||
}
|
||||
&--today {
|
||||
background: $pri;
|
||||
border: 2px solid $pri;
|
||||
box-shadow: 0 2px 8px rgba(196, 98, 58, 0.3);
|
||||
}
|
||||
&--empty {
|
||||
background: $surface-alt;
|
||||
}
|
||||
}
|
||||
|
||||
&__check {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
color: $acc;
|
||||
|
||||
.checkin-calendar__dot--today & {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
color: $tx3;
|
||||
font-weight: 400;
|
||||
|
||||
&--today {
|
||||
color: $tx;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&__tip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 14px;
|
||||
background: $acc-l;
|
||||
border-radius: $r-sm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-xs;
|
||||
}
|
||||
|
||||
&__tip-text {
|
||||
font-size: 12px;
|
||||
color: $acc;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// 长者模式
|
||||
.elder-mode & {
|
||||
&__dot {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
&__label {
|
||||
font-size: 13px;
|
||||
}
|
||||
&__tip-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
53
apps/miniprogram/src/components/ui/CheckinCalendar/index.tsx
Normal file
53
apps/miniprogram/src/components/ui/CheckinCalendar/index.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import './index.scss';
|
||||
|
||||
interface CheckinCalendarProps {
|
||||
consecutiveDays: number;
|
||||
earnedPoints?: number;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const DAYS = ['一', '二', '三', '四', '五', '六', '日'];
|
||||
|
||||
const CheckinCalendar: React.FC<CheckinCalendarProps> = ({
|
||||
consecutiveDays,
|
||||
}) => {
|
||||
const daysUntilReward = 7 - consecutiveDays;
|
||||
|
||||
return (
|
||||
<View className='checkin-calendar'>
|
||||
{DAYS.map((d, i) => {
|
||||
const isChecked = i < consecutiveDays;
|
||||
const isToday = i === consecutiveDays - 1;
|
||||
return (
|
||||
<View key={i} className='checkin-calendar__day'>
|
||||
<View
|
||||
className={`checkin-calendar__dot ${
|
||||
isChecked
|
||||
? isToday
|
||||
? 'checkin-calendar__dot--today'
|
||||
: 'checkin-calendar__dot--checked'
|
||||
: 'checkin-calendar__dot--empty'
|
||||
}`}
|
||||
>
|
||||
{isChecked && <Text className='checkin-calendar__check'>✓</Text>}
|
||||
</View>
|
||||
<Text className={`checkin-calendar__label ${isToday ? 'checkin-calendar__label--today' : ''}`}>
|
||||
周{d}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{daysUntilReward > 0 && (
|
||||
<View className='checkin-calendar__tip'>
|
||||
<Text className='checkin-calendar__tip-text'>
|
||||
再坚持 {daysUntilReward} 天,连续 7 天签到额外奖励 50 积分
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CheckinCalendar);
|
||||
142
apps/miniprogram/src/components/ui/CheckinModal/index.scss
Normal file
142
apps/miniprogram/src/components/ui/CheckinModal/index.scss
Normal file
@@ -0,0 +1,142 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.checkin-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
&__card {
|
||||
position: relative;
|
||||
width: 320px;
|
||||
background: $card;
|
||||
border-radius: $r;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__header {
|
||||
background: linear-gradient(135deg, $pri 0%, $pri-d 100%);
|
||||
padding: $sp-lg;
|
||||
padding-bottom: 20px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__header-deco {
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
right: -15px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--tk-font-body);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: $sp-xs;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__points-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: $sp-2xs;
|
||||
}
|
||||
|
||||
&__points-num {
|
||||
@include serif-number;
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: $white;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&__points-unit {
|
||||
font-size: var(--tk-font-body-sm);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&__streak {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin-top: $sp-xs;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__calendar {
|
||||
padding: $sp-section $sp-lg 0;
|
||||
}
|
||||
|
||||
&__calendar-title {
|
||||
font-size: var(--tk-font-body-sm);
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
margin-bottom: $sp-sm;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
&__calendar-body {
|
||||
position: relative;
|
||||
padding-bottom: 52px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 0 $sp-lg $sp-lg;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
width: 100%;
|
||||
padding: 12px 0;
|
||||
border-radius: $r-pill;
|
||||
background: $pri;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
box-shadow: $shadow-btn;
|
||||
@include touch-target;
|
||||
|
||||
&:active {
|
||||
opacity: var(--tk-touch-feedback-opacity);
|
||||
}
|
||||
}
|
||||
|
||||
&__btn-text {
|
||||
font-size: 15px;
|
||||
color: $white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
// 长者模式
|
||||
.elder-mode & {
|
||||
&__card {
|
||||
width: 340px;
|
||||
}
|
||||
&__points-num {
|
||||
font-size: 42px;
|
||||
}
|
||||
&__title {
|
||||
font-size: 17px;
|
||||
}
|
||||
&__btn {
|
||||
padding: 16px 0;
|
||||
}
|
||||
&__btn-text {
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
apps/miniprogram/src/components/ui/CheckinModal/index.tsx
Normal file
63
apps/miniprogram/src/components/ui/CheckinModal/index.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import CheckinCalendar from '../CheckinCalendar';
|
||||
import './index.scss';
|
||||
|
||||
interface CheckinModalProps {
|
||||
visible: boolean;
|
||||
consecutiveDays: number;
|
||||
earnedPoints: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const CheckinModal: React.FC<CheckinModalProps> = ({
|
||||
visible,
|
||||
consecutiveDays,
|
||||
earnedPoints,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<View className='checkin-modal'>
|
||||
<View className='checkin-modal__overlay' onClick={onClose} />
|
||||
<View className='checkin-modal__card'>
|
||||
{/* 顶部装饰区 */}
|
||||
<View className='checkin-modal__header'>
|
||||
<View className='checkin-modal__header-deco' />
|
||||
<Text className='checkin-modal__title'>签到成功</Text>
|
||||
<View className='checkin-modal__points-row'>
|
||||
<Text className='checkin-modal__points-num'>+{earnedPoints}</Text>
|
||||
<Text className='checkin-modal__points-unit'>积分</Text>
|
||||
</View>
|
||||
{consecutiveDays > 0 && (
|
||||
<Text className='checkin-modal__streak'>
|
||||
已连续签到 {consecutiveDays} 天
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* 7天日历 */}
|
||||
<View className='checkin-modal__calendar'>
|
||||
<Text className='checkin-modal__calendar-title'>本周签到</Text>
|
||||
<View className='checkin-modal__calendar-body'>
|
||||
<CheckinCalendar
|
||||
consecutiveDays={consecutiveDays}
|
||||
earnedPoints={earnedPoints}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 关闭按钮 */}
|
||||
<View className='checkin-modal__footer'>
|
||||
<View className='checkin-modal__btn' onClick={onClose}>
|
||||
<Text className='checkin-modal__btn-text'>我知道了</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CheckinModal);
|
||||
124
apps/miniprogram/src/components/ui/PointsCard/index.scss
Normal file
124
apps/miniprogram/src/components/ui/PointsCard/index.scss
Normal file
@@ -0,0 +1,124 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.points-card {
|
||||
background: linear-gradient(135deg, $pri 0%, $pri-d 100%);
|
||||
border-radius: $r;
|
||||
padding: $sp-lg;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: $sp-section;
|
||||
box-shadow: 0 8px 24px rgba(196, 98, 58, 0.25);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&__deco {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
|
||||
&--1 {
|
||||
top: -20px;
|
||||
right: -20px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
&--2 {
|
||||
bottom: -30px;
|
||||
right: 40px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
&--3 {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-bottom: $sp-xs;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
&__balance {
|
||||
@include serif-number;
|
||||
font-size: 42px;
|
||||
font-weight: 700;
|
||||
color: $white;
|
||||
line-height: 1;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: $sp-2xs;
|
||||
}
|
||||
|
||||
&__streak {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
&__checkin {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $sp-2xs;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
padding: 8px 16px;
|
||||
border-radius: $r-pill;
|
||||
cursor: pointer;
|
||||
|
||||
@include touch-target;
|
||||
|
||||
&:active {
|
||||
opacity: var(--tk-touch-feedback-opacity);
|
||||
}
|
||||
|
||||
&--done {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&__checkin-text {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: $white;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__checkin--done &__checkin-text {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
// 长者模式
|
||||
.elder-mode & {
|
||||
&__balance {
|
||||
font-size: 52px;
|
||||
}
|
||||
&__label {
|
||||
font-size: 15px;
|
||||
}
|
||||
&__checkin {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
&__checkin-text {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
apps/miniprogram/src/components/ui/PointsCard/index.tsx
Normal file
50
apps/miniprogram/src/components/ui/PointsCard/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import './index.scss';
|
||||
|
||||
interface PointsCardProps {
|
||||
balance: number;
|
||||
consecutiveDays: number;
|
||||
checkedIn: boolean;
|
||||
checkinLoading?: boolean;
|
||||
onCheckin?: () => void;
|
||||
}
|
||||
|
||||
const PointsCard: React.FC<PointsCardProps> = ({
|
||||
balance,
|
||||
consecutiveDays,
|
||||
checkedIn,
|
||||
checkinLoading = false,
|
||||
onCheckin,
|
||||
}) => {
|
||||
return (
|
||||
<View className='points-card'>
|
||||
{/* 装饰圆 */}
|
||||
<View className='points-card__deco points-card__deco--1' />
|
||||
<View className='points-card__deco points-card__deco--2' />
|
||||
<View className='points-card__deco points-card__deco--3' />
|
||||
|
||||
<View className='points-card__body'>
|
||||
<View className='points-card__left'>
|
||||
<Text className='points-card__label'>我的积分</Text>
|
||||
<Text className='points-card__balance'>{balance.toLocaleString()}</Text>
|
||||
{consecutiveDays > 0 && (
|
||||
<Text className='points-card__streak'>
|
||||
已连续签到 {consecutiveDays} 天
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
className={`points-card__checkin ${checkedIn ? 'points-card__checkin--done' : ''}`}
|
||||
onClick={() => !checkedIn && !checkinLoading && onCheckin?.()}
|
||||
>
|
||||
<Text className='points-card__checkin-text'>
|
||||
{checkinLoading ? '...' : checkedIn ? '已签到' : '签到'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PointsCard);
|
||||
116
apps/miniprogram/src/components/ui/ProductCard/index.scss
Normal file
116
apps/miniprogram/src/components/ui/ProductCard/index.scss
Normal file
@@ -0,0 +1,116 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../styles/mixins.scss';
|
||||
|
||||
.product-card {
|
||||
background: $card;
|
||||
border-radius: $r-sm;
|
||||
overflow: hidden;
|
||||
box-shadow: $shadow-sm;
|
||||
|
||||
@include touch-feedback;
|
||||
|
||||
&__thumb-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
@include flex-center;
|
||||
flex-direction: column;
|
||||
gap: $sp-2xs;
|
||||
|
||||
&--physical { background: $pri-l; }
|
||||
&--service { background: $acc-l; }
|
||||
&--privilege { background: $wrn-l; }
|
||||
}
|
||||
|
||||
&__thumb-type {
|
||||
font-size: var(--tk-font-cap);
|
||||
color: $tx3;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__soldout {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
@include flex-center;
|
||||
}
|
||||
|
||||
&__soldout-text {
|
||||
font-size: var(--tk-font-body-sm);
|
||||
color: $tx3;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__info {
|
||||
padding: 10px $sp-sm 14px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: var(--tk-font-body-sm);
|
||||
font-weight: 600;
|
||||
color: $tx;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
height: 40px;
|
||||
margin-bottom: $sp-xs;
|
||||
}
|
||||
|
||||
&__bottom {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: $sp-2xs;
|
||||
}
|
||||
|
||||
&__points-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__points-num {
|
||||
@include serif-number;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: $pri;
|
||||
}
|
||||
|
||||
&__points-unit {
|
||||
font-size: var(--tk-font-micro);
|
||||
color: $pri;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__low-stock {
|
||||
display: inline-block;
|
||||
font-size: var(--tk-font-micro);
|
||||
color: $wrn;
|
||||
font-weight: 500;
|
||||
background: $wrn-l;
|
||||
padding: 2px $sp-xs;
|
||||
border-radius: $r-sm;
|
||||
margin-top: $sp-2xs;
|
||||
}
|
||||
|
||||
// 长者模式
|
||||
.elder-mode & {
|
||||
&__name {
|
||||
font-size: 16px;
|
||||
height: 46px;
|
||||
}
|
||||
&__points-num {
|
||||
font-size: 22px;
|
||||
}
|
||||
&__points-unit {
|
||||
font-size: 13px;
|
||||
}
|
||||
&__low-stock {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
apps/miniprogram/src/components/ui/ProductCard/index.tsx
Normal file
58
apps/miniprogram/src/components/ui/ProductCard/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { View, Text } from '@tarojs/components';
|
||||
import type { PointsProduct } from '@/services/points';
|
||||
import './index.scss';
|
||||
|
||||
interface ProductCardProps {
|
||||
product: PointsProduct;
|
||||
onPress?: (product: PointsProduct) => void;
|
||||
}
|
||||
|
||||
const TYPE_BG_CLASS: Record<string, string> = {
|
||||
physical: 'product-card__thumb--physical',
|
||||
service: 'product-card__thumb--service',
|
||||
privilege: 'product-card__thumb--privilege',
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<string, string> = {
|
||||
physical: '实物',
|
||||
service: '服务券',
|
||||
privilege: '权益',
|
||||
};
|
||||
|
||||
const ProductCard: React.FC<ProductCardProps> = ({ product, onPress }) => {
|
||||
const isSoldOut = product.stock <= 0;
|
||||
const isLowStock = product.stock > 0 && product.stock <= 10;
|
||||
|
||||
return (
|
||||
<View
|
||||
className='product-card'
|
||||
onClick={() => onPress?.(product)}
|
||||
>
|
||||
<View className='product-card__thumb-wrap'>
|
||||
<View className={`product-card__thumb ${TYPE_BG_CLASS[product.product_type] || ''}`}>
|
||||
<Text className='product-card__thumb-type'>{TYPE_LABELS[product.product_type] || '商品'}</Text>
|
||||
</View>
|
||||
{isSoldOut && (
|
||||
<View className='product-card__soldout'>
|
||||
<Text className='product-card__soldout-text'>已兑完</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<View className='product-card__info'>
|
||||
<Text className='product-card__name'>{product.name}</Text>
|
||||
<View className='product-card__bottom'>
|
||||
<View className='product-card__points-row'>
|
||||
<Text className='product-card__points-num'>{product.points_cost}</Text>
|
||||
<Text className='product-card__points-unit'>积分</Text>
|
||||
</View>
|
||||
</View>
|
||||
{isLowStock && (
|
||||
<Text className='product-card__low-stock'>仅剩{product.stock}件</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ProductCard);
|
||||
Reference in New Issue
Block a user