@yorkjs/hive-ui 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "@yorkjs/hive-ui",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "Hive UI - Taro React component library",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
7
7
  "types": "src/index.ts",
8
+ "sideEffects": [
9
+ "**/*.styl",
10
+ "**/*.css"
11
+ ],
8
12
  "files": [
9
13
  "src"
10
14
  ],
@@ -0,0 +1,68 @@
1
+ @import '../../styles/mixin.styl'
2
+
3
+ .checkbox-component
4
+ display flex
5
+ flex-direction row
6
+ align-items center
7
+ cursor pointer
8
+ position relative
9
+
10
+ &:active
11
+ &:not(.is-disabled)
12
+ opacity 0.7
13
+
14
+ &:after
15
+ position absolute
16
+ content ''
17
+ top -6PX
18
+ bottom -6PX
19
+ left -6PX
20
+ right -6PX
21
+
22
+ // 选中状态文字颜色
23
+ &.is-checked
24
+ .label-text
25
+ color var(--primary)
26
+
27
+ // ✨ 禁用状态逻辑:不使用透明度
28
+ &.is-disabled
29
+ cursor not-allowed
30
+ .label-text
31
+ color var(--textPlaceholder)
32
+
33
+ // 大尺寸样式
34
+ &.size-large
35
+ .checkbox-icon-container
36
+ width 36PX
37
+ height 36PX
38
+ .label-text
39
+ font-size 16PX
40
+ margin-left 12PX
41
+
42
+ &.size-small
43
+ .checkbox-icon-container
44
+ width 16PX
45
+ height 16PX
46
+ .label-text
47
+ font-size 12PX
48
+ margin-left 6PX
49
+
50
+ // 图标容器
51
+ .checkbox-icon-container
52
+ display flex
53
+ align-items center
54
+ justify-content center
55
+ width 20PX
56
+ height 20PX
57
+ line-height 1
58
+ flex-shrink 0 // 防止被压缩
59
+ overflow visible
60
+
61
+ // 文字样式
62
+ .label-text
63
+ color var(--textContent)
64
+ font-size 13PX
65
+ margin-left 6PX
66
+ line-height 1
67
+ &.is-app-android
68
+ padding-top 1.5PX
@@ -0,0 +1,118 @@
1
+ import React, { useMemo } from 'react'
2
+ import { View, Text } from '@tarojs/components'
3
+
4
+ import { isAndroid, isApp } from '../../util/env'
5
+ import { formatClassNames } from '../../util/function'
6
+
7
+ import Icon from '../Icon'
8
+
9
+ import styles from './index.module.styl'
10
+
11
+ export interface CheckboxProps {
12
+ label?: string
13
+ checked: boolean
14
+ disabled?: boolean
15
+ readOnly?: boolean
16
+ size?: 'normal' | 'large' | 'small'
17
+ onChange?: (checked: boolean) => void
18
+ className?: string
19
+ }
20
+
21
+ const isDark = false
22
+
23
+ const Checkbox: React.FC<CheckboxProps> = (props) => {
24
+ const {
25
+ label,
26
+ checked = false,
27
+ disabled = false,
28
+ readOnly = false,
29
+ size = 'normal',
30
+ onChange,
31
+ className,
32
+ } = props
33
+
34
+ const handlePress = (e: any) => {
35
+ if (disabled || readOnly) return
36
+ onChange?.(!checked)
37
+ }
38
+
39
+ const iconSize = size === 'large' ? 36 : (size === 'small' ? 16 : 20)
40
+
41
+ const getIconColor = () => {
42
+ if (disabled) return 'var(--extraColor)'
43
+ if (checked) return 'var(--primary)'
44
+ return isDark ? '#ffffff' : 'var(--textPlaceholder)'
45
+ }
46
+
47
+ const iconName = checked ? 'check-circle-fill' : 'circle'
48
+
49
+ const iconNode = (
50
+ <Icon
51
+ name={iconName}
52
+ size={`${iconSize}PX`}
53
+ color={getIconColor()}
54
+ />
55
+ )
56
+
57
+ const iconElement = useMemo(() => {
58
+ if (disabled) {
59
+ return (
60
+ <View
61
+ style={{
62
+ width: `${iconSize}PX`,
63
+ height: `${iconSize}PX`,
64
+ borderRadius: `${iconSize / 2}PX`,
65
+ backgroundColor: 'var(--containerInner)',
66
+ display: 'flex',
67
+ alignItems: 'center',
68
+ justifyContent: 'center',
69
+ boxSizing: 'border-box'
70
+ }}
71
+ >
72
+ <Icon
73
+ name={iconName}
74
+ size={`${iconSize}PX`}
75
+ color={checked ? 'var(--extraColor)' : 'var(--textPlaceholder)'}
76
+ />
77
+ </View>
78
+ )
79
+ }
80
+ return iconNode
81
+ }, [disabled, checked, iconName, iconSize])
82
+
83
+ return (
84
+ <View
85
+ onClick={handlePress}
86
+ className={formatClassNames(
87
+ styles['checkbox-component'],
88
+ {
89
+ [styles['is-checked']]: checked,
90
+ [styles['is-disabled']]: disabled,
91
+ [styles['size-large']]: size === 'large',
92
+ [styles['size-small']]: size === 'small',
93
+ },
94
+ className
95
+ )}
96
+ >
97
+ <View className={styles['checkbox-icon-container']}>
98
+ {iconElement}
99
+ </View>
100
+
101
+ {/* TODO: 临时解决 app 上 Android文字偏上对齐问题 */}
102
+ {label && (
103
+ <Text
104
+ className={formatClassNames(
105
+ styles['label-text'],
106
+ {
107
+ [styles['is-app-android']]: !!(isApp && isAndroid)
108
+ }
109
+ )}
110
+ >
111
+ {label}
112
+ </Text>
113
+ )}
114
+ </View>
115
+ )
116
+ }
117
+
118
+ export default React.memo(Checkbox)
@@ -0,0 +1,29 @@
1
+ @import '../../styles/mixin.styl'
2
+
3
+ .tag-component
4
+ display inline-flex
5
+ align-items center
6
+ justify-content center
7
+ padding 0 8PX
8
+ height 20PX
9
+ line-height 20PX
10
+ border-radius 4PX
11
+ text-align center
12
+ font-size 12PX
13
+ white-space nowrap
14
+ box-sizing border-box
15
+
16
+ halfBorder(top left right bottom, transparent, 8PX)
17
+
18
+ .tag-text
19
+ line-height 1
20
+
21
+ .type-default
22
+ &:before
23
+ border-color var(--lineBorder)
24
+ :global(.is-dark)
25
+ &:before
26
+ border-color transparent
27
+
28
+ .type-primary, .type-info, .type-success, .type-error
29
+ border none
@@ -0,0 +1,117 @@
1
+ import React, { useMemo } from 'react'
2
+ import { View, Text } from '@tarojs/components'
3
+
4
+ import { formatClassNames } from '../../util/function'
5
+
6
+ import styles from './index.module.styl'
7
+
8
+ export type TagType = 'default'
9
+ | 'success'
10
+ | 'info'
11
+ | 'error'
12
+ | 'primary'
13
+ | 'warning'
14
+ | 'light-info'
15
+ | 'light-primary'
16
+ | 'light-success'
17
+ | 'light-error'
18
+ | 'light-warning'
19
+
20
+ interface TagProps {
21
+ type?: TagType
22
+ text?: string | React.ReactNode
23
+ className?: string
24
+ style?: React.CSSProperties
25
+ }
26
+ const isDark = false
27
+ const Tag: React.FC<TagProps> = ({
28
+ type = 'default',
29
+ text,
30
+ className,
31
+ style
32
+ }) => {
33
+
34
+ const themeSelect = (light: string, dark: string) => (isDark ? dark : light)
35
+
36
+ const tagStyle = useMemo(() => {
37
+ const colors = {
38
+ primary: 'var(--primary)',
39
+ success: 'var(--success)',
40
+ error: 'var(--error)',
41
+ info: 'var(--textTitle)',
42
+ warning: 'var(--warning)',
43
+ }
44
+
45
+ const mapping = {
46
+ default: {
47
+ color: 'var(--textTitle)',
48
+ backgroundColor: themeSelect('rgba(255, 255, 255, 0.16)', 'var(--containerInner)'),
49
+ },
50
+ info: {
51
+ color: 'var(--textTitle)',
52
+ backgroundColor: themeSelect('rgba(245, 245, 245, 1)', 'var(--containerInner)'),
53
+ },
54
+ 'light-info': {
55
+ color: 'var(--textMuted)',
56
+ backgroundColor: themeSelect('rgba(245, 245, 245, 1)', 'var(--containerMedium)'),
57
+ },
58
+ warning: {
59
+ color: '#FFFFFF',
60
+ backgroundColor: colors.warning,
61
+ },
62
+ 'light-warning': {
63
+ color: colors.warning,
64
+ backgroundColor: 'rgba(255,103,0,0.2)',
65
+ },
66
+ primary: {
67
+ color: '#FFFFFF',
68
+ backgroundColor: colors.primary,
69
+ },
70
+ 'light-primary': {
71
+ color: colors.primary,
72
+ backgroundColor: 'rgba(255, 103, 0, 0.2)',
73
+ },
74
+ success: {
75
+ color: '#FFFFFF',
76
+ backgroundColor: colors.success,
77
+ },
78
+ 'light-success': {
79
+ color: colors.success,
80
+ backgroundColor: 'rgba(3, 190, 2, 0.2)',
81
+ },
82
+ error: {
83
+ color: '#FFFFFF',
84
+ backgroundColor: colors.error,
85
+ },
86
+ 'light-error': {
87
+ color: colors.error,
88
+ backgroundColor: 'rgba(255, 49, 65, 0.2)',
89
+ },
90
+ }
91
+
92
+ return mapping[type] || mapping.default
93
+ }, [type])
94
+
95
+ return (
96
+ <View
97
+ className={formatClassNames(
98
+ styles['tag-component'],
99
+ styles[`type-${type}`],
100
+ className
101
+ )}
102
+ style={{
103
+ backgroundColor: tagStyle.backgroundColor,
104
+ color: tagStyle.color,
105
+ ...style
106
+ }}
107
+ >
108
+ <Text
109
+ className={styles['tag-text']}
110
+ >
111
+ {text}
112
+ </Text>
113
+ </View>
114
+ )
115
+ }
116
+
117
+ export default Tag
@@ -0,0 +1,22 @@
1
+ // 商品类型
2
+ export const MALL_PRODUCT_TYPE_REAL = 1
3
+ export const MALL_PRODUCT_TYPE_VIRTUAL = 2
4
+ export const MALL_PRODUCT_TYPE_SERVICE = 3
5
+
6
+ // 快递发货
7
+ export const MALL_PRODUCT_DELIVERY_TYPE_EXPRESS = 1
8
+ // 到店自提
9
+ export const MALL_PRODUCT_DELIVERY_TYPE_SELF = 2
10
+ // 门店配送
11
+ export const MALL_PRODUCT_DELIVERY_TYPE_SHOP = 3
12
+ // 堂食
13
+ export const MALL_PRODUCT_DELIVERY_TYPE_DINE = 4
14
+
15
+ // 秒杀活动
16
+ export const PROMOTION_ACTIVITY_TYPE_FLASH = 1
17
+ // 品牌活动
18
+ export const PROMOTION_ACTIVITY_TYPE_BRAND = 2
19
+ // 热点活动
20
+ export const PROMOTION_ACTIVITY_TYPE_HOT = 3
21
+ //买赠活动
22
+ export const PROMOTION_ACTIVITY_TYPE_PRESENT = 4
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { default as NumberKeyboard } from './components/NumberKeyboard'
2
2
  export { default as Cell, setCellConfig } from './components/Cell'
3
3
  export { default as Icon } from './components/Icon'
4
+ export { default as Checkbox } from './components/Checkbox'
5
+ export { default as Tag } from './components/Tag'
4
6
 
5
- export { default as FlowItem } from './item/FlowItem'
7
+ export { default as FlowItem } from './item/FlowItem'
8
+ export { default as ProductCard } from './item/ProductCard'
@@ -0,0 +1,251 @@
1
+ @import '../../styles/mixin.styl'
2
+
3
+ // 颜色与字号变量对照表 (基于 RN getStyles)
4
+ $F_TITLE = 15PX // RN 15
5
+ $F_PRICE_VAL = 14PX // RN 14
6
+ $F_PRICE_SYM = 10PX // RN 10
7
+ $F_INFO = 12PX // RN 12 (库存/销量/条码)
8
+ $F_TAG_TYPE = 11PX // RN 11 (商品类型实物/虚拟/服务)
9
+ $F_TAG_NORMAL = 10PX // RN 10 (活动标签/配送方式)
10
+ $F_SPEC = 11PX // RN 11
11
+ $F_CUSTOM = 10PX // RN 10 (做法描述)
12
+ $F_BUY_COUNT = 14PX // RN 14
13
+
14
+ .item-container
15
+ position relative
16
+ display flex
17
+ flex-direction column
18
+ padding 15PX 10PX // 对齐 RN: paddingVertical: 15, paddingHorizontal: 10
19
+ background-color var(--containerCard)
20
+ box-sizing border-box
21
+
22
+ .card-main-body
23
+ display flex
24
+ flex-direction row
25
+ align-items flex-start
26
+ width 100%
27
+
28
+ .checkbox-container
29
+ height 72PX // 对齐图片高度 72
30
+ margin-right 10PX
31
+ display flex
32
+ justify-content center
33
+ align-items center
34
+ flex-shrink 0
35
+
36
+ .image-container
37
+ position relative
38
+ margin-right 10PX
39
+ flex-shrink 0
40
+ width 72PX
41
+ height 72PX
42
+
43
+ .product-image
44
+ width 72PX
45
+ height 72PX
46
+ border-radius 10PX // 对齐 RN: IMAGE_RADIUS = 10
47
+ background-color var(--containerInner)
48
+
49
+ .status-overlay
50
+ position absolute
51
+ top 0
52
+ left 0
53
+ bottom 0
54
+ right 0
55
+ border-radius 10PX
56
+ background-color rgba(0, 0, 0, 0.3)
57
+ display flex
58
+ justify-content center
59
+ align-items center
60
+ z-index 2
61
+
62
+ .status-text
63
+ font-size 12PX // 对齐 RN: 12
64
+ color #FFF
65
+ line-height 1
66
+
67
+ .content-container
68
+ flex 1
69
+ display flex
70
+ flex-direction column
71
+ justify-content space-between
72
+ min-height 72PX
73
+ min-width 0 // 关键:确保内部 Flex 截断能正常工作
74
+
75
+ .content-body
76
+ flex 1
77
+ display flex
78
+ flex-direction column
79
+
80
+ .title-row
81
+ display flex
82
+ flex-direction row
83
+ align-items center
84
+ margin-bottom 2PX
85
+
86
+ .product-tag-container
87
+ display flex
88
+ margin-right 8PX
89
+ flex-shrink 0
90
+ line-height 1
91
+
92
+ .product-tag
93
+ height 15PX
94
+ line-height 15PX
95
+ padding 1PX 3PX
96
+ border-radius 3PX
97
+ font-size 9PX
98
+
99
+ .title
100
+ flex 1
101
+ font-size $F_TITLE
102
+ color var(--textTitle)
103
+ line-height 1.4
104
+ font-weight bold
105
+ lineClamp(1)
106
+
107
+ .second-row
108
+ margin-top 2PX
109
+ width 100%
110
+ white-space nowrap
111
+ .second-row-inner
112
+ display flex
113
+ flex-direction row
114
+ align-items center
115
+
116
+ .info-line
117
+ width 1PX
118
+ height 10PX
119
+ background-color var(--textContent)
120
+ margin 0 5PX
121
+ transform scaleX(0.5)
122
+
123
+ .info-text
124
+ font-size $F_INFO
125
+ color var(--textTitle)
126
+
127
+ .primary-text
128
+ font-size $F_INFO
129
+ color var(--primary)
130
+
131
+ .stock-and-sale
132
+ display flex
133
+ flex-direction row
134
+ align-items center
135
+ .stock-box
136
+ display flex
137
+ flex-direction row
138
+ margin-right 5PX
139
+ .stock-text
140
+ font-size $F_INFO
141
+ color var(--primary)
142
+ margin-left 5PX
143
+
144
+ .tag-row
145
+ display flex
146
+ flex-direction row
147
+ align-items center
148
+ flex-wrap wrap
149
+ gap 5PX
150
+ margin-top 5PX
151
+
152
+ .delivery-tag
153
+ padding 1PX 5PX
154
+ border-radius 3PX
155
+ font-size $F_TAG_NORMAL
156
+ border 1PX solid transparent
157
+
158
+ .activity-tag
159
+ display flex
160
+ flex-direction row
161
+ align-items center
162
+ border 1PX solid #F21902 // 对齐 RN: borderColor: '#F21902'
163
+ border-radius 3PX
164
+ overflow hidden
165
+
166
+ .activity-tag-badge
167
+ background-color #F21902
168
+ padding 1PX 4PX // 对齐 RN: paddingHorizontal: 4, paddingVertical: 1
169
+ display flex
170
+ align-items center
171
+ justify-content center
172
+
173
+ .activity-tag-badge-text
174
+ font-size $F_TAG_NORMAL
175
+ color #FFF
176
+ line-height 1
177
+
178
+ .activity-tag-content
179
+ padding 1PX 5PX // 对齐 RN: paddingHorizontal: 5, paddingVertical: 1
180
+
181
+ .activity-tag-text
182
+ font-size $F_TAG_NORMAL
183
+ color #F21902
184
+ line-height 1
185
+
186
+ .spec-text
187
+ font-size $F_SPEC
188
+ color var(--textContent)
189
+ line-height 15PX // 对齐 RN: lineHeight: 15
190
+ margin-top 5PX // 对齐 RN: marginTop: 5
191
+ lineClamp(1)
192
+
193
+ .customization-text
194
+ font-size $F_CUSTOM
195
+ color var(--textMuted)
196
+ line-height 15PX // 对齐 RN: lineHeight: 15
197
+ margin-top 5PX // 对齐 RN: marginTop: 5
198
+ lineClamp(3)
199
+
200
+ .content-bottom
201
+ display flex
202
+ flex-direction row
203
+ align-items center
204
+ justify-content space-between
205
+ margin-top 10PX // 对齐 RN: marginTop: 10
206
+
207
+ .price-container
208
+ display flex
209
+ flex-direction row
210
+ align-items center
211
+
212
+ .price-row
213
+ display flex
214
+ flex-direction row
215
+ align-items baseline // 确保符号和数值底部对齐
216
+
217
+ .price-symbol
218
+ font-size $F_PRICE_SYM
219
+ color var(--textMoney)
220
+ font-weight bold // 对齐 RN: FONT_WEIGHT_BOLD
221
+ margin-right 1PX
222
+
223
+ .price-value
224
+ font-size $F_PRICE_VAL
225
+ color var(--textMoney)
226
+ font-weight bold // 对齐 RN: FONT_WEIGHT_BOLD
227
+
228
+ .original-price
229
+ font-size 11PX // 对齐 RN: 11
230
+ color var(--textMuted)
231
+ text-decoration line-through
232
+ margin-left 5PX // 对齐 RN: marginLeft: 5
233
+
234
+ .buy-count-value
235
+ font-size $F_BUY_COUNT
236
+ color var(--textContent)
237
+ margin-left 10PX // 对齐 RN: marginLeft: 10
238
+
239
+ .delete-container
240
+ position absolute
241
+ top 16PX // 对齐 RN: top: 16
242
+ right 11PX // 对齐 RN: right: 11
243
+ padding 5PX // 对齐 RN: pressHitSlop
244
+ z-index 10
245
+
246
+ .delete-icon
247
+ color var(--textContent)
248
+
249
+ .card-footer
250
+ width 100%
251
+ margin-top 12PX
@@ -0,0 +1,300 @@
1
+ import React, { useMemo } from 'react'
2
+ import { View, Text, ScrollView, Image, ITouchEvent } from '@tarojs/components'
3
+
4
+ import { formatClassNames, isEmpty } from '../../util/function'
5
+
6
+ import Tag from '../../components/Tag'
7
+ import Icon from '../../components/Icon'
8
+ import Checkbox from '../../components/Checkbox'
9
+
10
+ import styles from './index.module.styl'
11
+
12
+ import {
13
+ MALL_PRODUCT_TYPE_REAL,
14
+ MALL_PRODUCT_TYPE_VIRTUAL,
15
+ MALL_PRODUCT_TYPE_SERVICE,
16
+ MALL_PRODUCT_DELIVERY_TYPE_EXPRESS,
17
+ MALL_PRODUCT_DELIVERY_TYPE_SELF,
18
+ MALL_PRODUCT_DELIVERY_TYPE_SHOP,
19
+ MALL_PRODUCT_DELIVERY_TYPE_DINE,
20
+ PROMOTION_ACTIVITY_TYPE_FLASH,
21
+ PROMOTION_ACTIVITY_TYPE_BRAND,
22
+ PROMOTION_ACTIVITY_TYPE_HOT,
23
+ PROMOTION_ACTIVITY_TYPE_PRESENT,
24
+ } from '../../constant/mall'
25
+
26
+ export interface IProductCardLabels {
27
+ product_type?: { real: string; virtual: string; service: string }
28
+ delivery_type?: { dine: string; self_pickup: string; delivery: string }
29
+ activity_type?: { flash: string; brand: string; hot: string; present: string }
30
+ present_badge?: string
31
+ stock_label?: string
32
+ sale_label?: string
33
+ multiple_spec?: string
34
+ barcode_label?: string
35
+ spec_label?: string
36
+ sold_out?: string
37
+ offline?: string
38
+ }
39
+
40
+ /** 商品卡片组件属性 */
41
+ export interface IProductCardProps {
42
+ /** 外部样式类 */
43
+ className?: string
44
+ /** 商品图片对象 */
45
+ image: { url: string }
46
+ /** 是否显示售罄标记 */
47
+ showSoldOut?: boolean
48
+ /** 是否显示已下架标记 */
49
+ showOffline?: boolean
50
+ /** 商品标题 */
51
+ title: string
52
+ /** 标题最大显示行数 (默认1) */
53
+ titleMaxLines?: number
54
+ /** 商品类型:实物/虚拟/服务 */
55
+ productType?: number
56
+ /** 是否显示“多规格”标签 */
57
+ showMultipleSpec?: boolean
58
+ /** 库存数量文案 */
59
+ stockCount?: string | number
60
+ /** 销量文案 */
61
+ saleCount?: string | number
62
+ /** 商品条码 */
63
+ barcode?: string
64
+ /** 规格描述文案 (如: 红色, 大号) */
65
+ spec?: string
66
+ /** 规格描述最大显示行数 (默认1) */
67
+ specMaxLines?: number
68
+ /** 活动类型:秒杀/品牌/热点/满赠 */
69
+ activityType?: number
70
+ /** 配送方式集合:堂食/自提/外送 */
71
+ deliveryTypes?: number[]
72
+ /** 做法/定制化信息描述 */
73
+ customization?: string
74
+ /** 做法描述最大显示行数 (默认3) */
75
+ customizationMaxLines?: number
76
+ /** 售价 */
77
+ salePrice: string | number
78
+ /** 原价 (划线价) */
79
+ originalPrice?: string | number
80
+ /** 已购数量 (显示为 xN) */
81
+ buyCount?: number
82
+ /** 是否显示左侧勾选框 */
83
+ showCheckbox?: boolean
84
+ /** 勾选框是否选中 */
85
+ checkboxChecked?: boolean
86
+ /** 勾选框是否禁用 */
87
+ checkboxDisabled?: boolean
88
+ /** 点击勾选框或卡片触发 (当 checkbox 可用时) */
89
+ onCheckboxClick?: () => void
90
+ /** 是否显示右上角删除按钮 */
91
+ showDelete?: boolean
92
+ /** 点击删除按钮回调 */
93
+ onDelete?: () => void
94
+ /** 右下角自定义操作区域 (与 buyCount 互斥) */
95
+ action?: React.ReactNode
96
+ /** 国际化标签注入 */
97
+ labels?: IProductCardLabels
98
+ // 卡片点击事件
99
+ onClick?: () => void
100
+ children?: React.ReactNode
101
+ }
102
+
103
+ const DEFAULT_LABELS: IProductCardLabels = {
104
+ product_type: { real: '实物', virtual: '虚拟', service: '服务' },
105
+ delivery_type: { dine: '堂食', self_pickup: '到店自取', delivery: '送货上门' },
106
+ activity_type: { flash: '秒杀活动', brand: '品牌活动', hot: '热点活动', present: '买赠活动' },
107
+ present_badge: '赠',
108
+ stock_label: '库存',
109
+ sale_label: '销量',
110
+ multiple_spec: '多规格',
111
+ barcode_label: '条码',
112
+ spec_label: '规格',
113
+ sold_out: '售罄',
114
+ offline: '已下架'
115
+ }
116
+
117
+ const ProductCard: React.FC<IProductCardProps> = (props) => {
118
+ const {
119
+ image,
120
+ title,
121
+ titleMaxLines = 1,
122
+ productType,
123
+ showMultipleSpec,
124
+ stockCount,
125
+ saleCount,
126
+ barcode,
127
+ spec,
128
+ specMaxLines = 1,
129
+ activityType,
130
+ deliveryTypes = [],
131
+ customization,
132
+ customizationMaxLines = 3,
133
+ salePrice,
134
+ originalPrice,
135
+ buyCount,
136
+ showSoldOut = false,
137
+ showOffline = false,
138
+ showCheckbox = false,
139
+ checkboxChecked = false,
140
+ checkboxDisabled = false,
141
+ onCheckboxClick,
142
+ showDelete,
143
+ onDelete,
144
+ action,
145
+ className,
146
+ labels: customLabels,
147
+ onClick,
148
+ children
149
+ } = props
150
+
151
+ const handleCardClick = (e: ITouchEvent) => {
152
+ if (showCheckbox) {
153
+ if (!checkboxDisabled) {
154
+ onCheckboxClick?.()
155
+ }
156
+ }
157
+ else {
158
+ onClick?.()
159
+ }
160
+ }
161
+
162
+ const labels = useMemo(() => ({
163
+ ...DEFAULT_LABELS,
164
+ ...customLabels,
165
+ product_type: { ...DEFAULT_LABELS.product_type, ...customLabels?.product_type },
166
+ delivery_type: { ...DEFAULT_LABELS.delivery_type, ...customLabels?.delivery_type },
167
+ activity_type: { ...DEFAULT_LABELS.activity_type, ...customLabels?.activity_type }
168
+ }), [customLabels])
169
+
170
+ const productTypeTag = useMemo(() => {
171
+ if (productType === undefined) return null
172
+ const colorMap: Record<number, { color: string; text: string | undefined }> = {
173
+ [MALL_PRODUCT_TYPE_REAL]: { color: 'var(--primary)', text: labels.product_type?.real },
174
+ [MALL_PRODUCT_TYPE_VIRTUAL]: { color: '#E4B700', text: labels.product_type?.virtual },
175
+ [MALL_PRODUCT_TYPE_SERVICE]: { color: '#03BE02', text: labels.product_type?.service },
176
+ }
177
+ const item = colorMap[productType]
178
+ if (!item) return null
179
+ return (
180
+ <View className={styles['product-tag-container']}>
181
+ <Tag
182
+ type='light-warning'
183
+ text={item.text}
184
+ className={styles['product-tag']}
185
+ style={{ backgroundColor: item.color, color: '#fff' }}
186
+ />
187
+ </View>
188
+ )
189
+ }, [productType, labels])
190
+
191
+ const deliveryTags = useMemo(() => {
192
+ if (isEmpty(deliveryTypes)) return null
193
+ return deliveryTypes.map(type => {
194
+ let tagProps: any = null
195
+ if (type === MALL_PRODUCT_DELIVERY_TYPE_DINE) {
196
+ tagProps = { text: labels.delivery_type?.dine, color: '#2db7f5', bg: 'rgba(145, 213, 255, 0.16)' }
197
+ } else if (type === MALL_PRODUCT_DELIVERY_TYPE_SELF) {
198
+ tagProps = { text: labels.delivery_type?.self_pickup, color: '#FF3D15', bg: 'rgba(255,61,21,0.16)' }
199
+ } else if (type === MALL_PRODUCT_DELIVERY_TYPE_EXPRESS || type === MALL_PRODUCT_DELIVERY_TYPE_SHOP) {
200
+ tagProps = { text: labels.delivery_type?.delivery, color: '#FF8901', bg: 'rgba(255,137,1,0.16)' }
201
+ }
202
+ if (!tagProps) return null
203
+ return (
204
+ <Tag
205
+ key={type}
206
+ text={tagProps.text}
207
+ style={{ color: tagProps.color, backgroundColor: tagProps.bg, borderColor: tagProps.color }}
208
+ className={styles['delivery-tag']}
209
+ />
210
+ )
211
+ })
212
+ }, [deliveryTypes, labels])
213
+
214
+ const stockAndSaleElem = useMemo(() => {
215
+ if (!stockCount && !saleCount) return null
216
+ return (
217
+ <View className={styles['stock-and-sale']}>
218
+ {stockCount && (
219
+ <View className={styles['stock-box']}>
220
+ <Text className={styles['info-text']}>{labels.stock_label}</Text>
221
+ <Text className={styles['stock-text']}>{stockCount}</Text>
222
+ </View>
223
+ )}
224
+ {saleCount && (
225
+ <Text className={styles['info-text']}>{labels.sale_label} {saleCount}</Text>
226
+ )}
227
+ </View>
228
+ )
229
+ }, [stockCount, saleCount, labels])
230
+
231
+ return (
232
+ <View className={formatClassNames(styles['item-container'], className)} onClick={handleCardClick}>
233
+ <View className={styles['card-main-body']}>
234
+ {showCheckbox && (
235
+ <View className={styles['checkbox-container']}>
236
+ <Checkbox checked={checkboxChecked} disabled={checkboxDisabled} readOnly />
237
+ </View>
238
+ )}
239
+ <View className={styles['image-container']}>
240
+ <Image src={image?.url} className={styles['product-image']} mode='aspectFill' />
241
+ {(showSoldOut || showOffline) && (
242
+ <View className={styles['status-overlay']}>
243
+ <Text className={styles['status-text']}>{showSoldOut ? labels.sold_out : labels.offline}</Text>
244
+ </View>
245
+ )}
246
+ </View>
247
+ <View className={styles['content-container']}>
248
+ <View className={styles['content-body']}>
249
+ <View className={styles['title-row']} style={{ marginRight: showDelete ? '30PX' : '0' }}>
250
+ {productTypeTag}
251
+ <Text className={styles['title']} style={{ WebkitLineClamp: titleMaxLines }}>{title}</Text>
252
+ </View>
253
+ {(showMultipleSpec || stockCount || saleCount || barcode) && (
254
+ <ScrollView scrollX className={styles['second-row']} showScrollbar={false}>
255
+ <View className={styles['second-row-inner']}>
256
+ {showMultipleSpec && <Text className={styles['primary-text']}>{labels.multiple_spec}</Text>}
257
+ {showMultipleSpec && stockAndSaleElem && <View className={styles['info-line']} />}
258
+ {stockAndSaleElem}
259
+ {((showMultipleSpec || stockAndSaleElem) && barcode) && <View className={styles['info-line']} />}
260
+ {barcode && <Text className={styles['info-text']}>{labels.barcode_label} {barcode}</Text>}
261
+ </View>
262
+ </ScrollView>
263
+ )}
264
+ {(activityType || deliveryTags) && (
265
+ <View className={styles['tag-row']}>
266
+ {activityType && labels.activity_type && (
267
+ <View className={styles['activity-tag']}>
268
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && <View className={styles['activity-tag-badge']}><Text className={styles['activity-tag-badge-text']}>{labels.present_badge}</Text></View>}
269
+ <View className={styles['activity-tag-content']}>
270
+ <Text className={styles['activity-tag-text']}>
271
+ {activityType === PROMOTION_ACTIVITY_TYPE_FLASH && labels.activity_type.flash}
272
+ {activityType === PROMOTION_ACTIVITY_TYPE_BRAND && labels.activity_type.brand}
273
+ {activityType === PROMOTION_ACTIVITY_TYPE_HOT && labels.activity_type.hot}
274
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && labels.activity_type.present}
275
+ </Text>
276
+ </View>
277
+ </View>
278
+ )}
279
+ {deliveryTags}
280
+ </View>
281
+ )}
282
+ {spec && <Text className={styles['spec-text']} style={{ WebkitLineClamp: specMaxLines }}>{labels.spec_label}:{spec}</Text>}
283
+ {customization && <Text className={styles['customization-text']} style={{ WebkitLineClamp: customizationMaxLines }}>{customization}</Text>}
284
+ </View>
285
+ <View className={styles['content-bottom']}>
286
+ <View className={styles['price-container']}>
287
+ <View className={styles['price-row']}><Text className={styles['price-symbol']}>¥</Text><Text className={styles['price-value']}>{salePrice}</Text></View>
288
+ {originalPrice && <Text className={styles['original-price']}>¥{originalPrice}</Text>}
289
+ </View>
290
+ <View className={styles['action-area']}>{buyCount ? <Text className={styles['buy-count-value']}>x{buyCount}</Text> : action}</View>
291
+ </View>
292
+ </View>
293
+ </View>
294
+ {children && <View className={styles['card-footer']} onClick={e => e.stopPropagation()}>{children}</View>}
295
+ {showDelete && <View className={styles['delete-container']} onClick={(e: ITouchEvent) => { e.stopPropagation(); onDelete?.(); }}><Icon name='trash' size={13} className={styles['delete-icon']} /></View>}
296
+ </View>
297
+ )
298
+ }
299
+
300
+ export default React.memo(ProductCard)
@@ -0,0 +1,17 @@
1
+ import Taro from "@tarojs/taro"
2
+
3
+ // 支付宝小程序
4
+ export const isAlipayApp = Taro.getEnv() === Taro.ENV_TYPE.ALIPAY
5
+ // 微信小程序
6
+ export const isWeApp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP
7
+ // web
8
+ export const isWeb = Taro.getEnv() === Taro.ENV_TYPE.WEB
9
+
10
+ const userAgent = navigator && navigator.userAgent
11
+ export const isIos = /iphone|ipad/i.test(userAgent)
12
+ export const isAndroid = /android/i.test(userAgent)
13
+
14
+ const promoterAppMatch = userAgent.match(/promoter_app\/([\d.]+)/i)
15
+
16
+ export const isPromoterApp = promoterAppMatch && (isIos || isAndroid)
17
+ export const isApp = isPromoterApp