@yorkjs/hive-ui 0.0.2 → 0.0.4

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.4",
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,31 @@
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, var(--tag-border-color, transparent), 8PX)
17
+
18
+ .tag-text
19
+ line-height 1
20
+
21
+ .type-default
22
+ --tag-border-color: var(--lineBorder)
23
+ :global(.is-dark)
24
+ --tag-border-color: transparent
25
+
26
+ .tag-component:before
27
+ border-color: var(--tag-border-color, transparent)
28
+
29
+
30
+ .type-primary, .type-info, .type-success, .type-error
31
+ border none
@@ -0,0 +1,126 @@
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
+ const themeSelect = (light: string, dark: string) => (isDark ? dark : light)
34
+
35
+ const tagStyle = useMemo(() => {
36
+ const colors = {
37
+ primary: 'var(--primary)',
38
+ success: 'var(--success)',
39
+ error: 'var(--error)',
40
+ info: 'var(--textTitle)',
41
+ warning: 'var(--warning)',
42
+ }
43
+
44
+ const mapping = {
45
+ default: {
46
+ color: 'var(--textTitle)',
47
+ backgroundColor: themeSelect('rgba(255, 255, 255, 0.16)', 'var(--containerInner)'),
48
+ },
49
+ info: {
50
+ color: 'var(--textTitle)',
51
+ backgroundColor: themeSelect('rgba(245, 245, 245, 1)', 'var(--containerInner)'),
52
+ },
53
+ 'light-info': {
54
+ color: 'var(--textMuted)',
55
+ backgroundColor: themeSelect('rgba(245, 245, 245, 1)', 'var(--containerMedium)'),
56
+ },
57
+ warning: {
58
+ color: '#FFFFFF',
59
+ backgroundColor: colors.warning,
60
+ },
61
+ 'light-warning': {
62
+ color: colors.warning,
63
+ backgroundColor: 'rgba(255,103,0,0.2)',
64
+ },
65
+ primary: {
66
+ color: '#FFFFFF',
67
+ backgroundColor: colors.primary,
68
+ },
69
+ 'light-primary': {
70
+ color: colors.primary,
71
+ backgroundColor: 'rgba(255, 103, 0, 0.2)',
72
+ },
73
+ success: {
74
+ color: '#FFFFFF',
75
+ backgroundColor: colors.success,
76
+ },
77
+ 'light-success': {
78
+ color: colors.success,
79
+ backgroundColor: 'rgba(3, 190, 2, 0.2)',
80
+ },
81
+ error: {
82
+ color: '#FFFFFF',
83
+ backgroundColor: colors.error,
84
+ },
85
+ 'light-error': {
86
+ color: colors.error,
87
+ backgroundColor: 'rgba(255, 49, 65, 0.2)',
88
+ },
89
+ }
90
+
91
+ return mapping[type] || mapping.default
92
+ }, [type])
93
+
94
+ const finalStyle = useMemo(() => {
95
+ const s: React.CSSProperties & { [key: string]: any } = {
96
+ backgroundColor: tagStyle.backgroundColor,
97
+ color: tagStyle.color,
98
+ ...style,
99
+ }
100
+
101
+ if (style?.borderColor) {
102
+ s['--tag-border-color'] = style.borderColor
103
+ }
104
+
105
+ return s
106
+ }, [tagStyle, style])
107
+
108
+ return (
109
+ <View
110
+ className={formatClassNames(
111
+ styles['tag-component'],
112
+ styles[`type-${type}`],
113
+ className
114
+ )}
115
+ style={finalStyle}
116
+ >
117
+ <Text
118
+ className={styles['tag-text']}
119
+ >
120
+ {text}
121
+ </Text>
122
+ </View>
123
+ )
124
+ }
125
+
126
+ 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,258 @@
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
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
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
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
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
+ align-items center
89
+ margin-right 5PX
90
+ flex-shrink 0
91
+ line-height 1
92
+
93
+ .product-tag
94
+ height 15PX
95
+ line-height 15PX
96
+ padding 1PX 3PX
97
+ border-radius 3PX
98
+ font-size 9PX
99
+
100
+ .title
101
+ flex 1
102
+ font-size $F_TITLE
103
+ color var(--textTitle)
104
+ line-height 1.4
105
+ font-weight bold
106
+ lineClamp(1)
107
+
108
+ .second-row
109
+ margin-top 2PX
110
+ width 100%
111
+ white-space nowrap
112
+ .second-row-inner
113
+ display flex
114
+ flex-direction row
115
+ align-items center
116
+
117
+ .info-line
118
+ width 1PX
119
+ height 10PX
120
+ margin 0 5PX
121
+ halfBorder(left, var(--textContent))
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
+ height 15PX
157
+ line-height 15PX
158
+
159
+ .activity-tag
160
+ display flex
161
+ flex-direction row
162
+ align-items center
163
+ halfBorder(top left right bottom, #F21902, 6PX)
164
+ height 15PX
165
+ line-height 15PX
166
+ border-radius 3PX
167
+ overflow hidden
168
+
169
+ .activity-tag-badge
170
+ padding 0 3PX
171
+ height 100%
172
+ background-color #F21902
173
+ border-top-left-radius 3PX
174
+ border-bottom-right-radius 3PX
175
+ display flex
176
+ align-items center
177
+ justify-content center
178
+
179
+ .activity-tag-badge-text
180
+ font-size $F_TAG_NORMAL
181
+ color #FFF
182
+ line-height 1
183
+
184
+ .activity-tag-content
185
+ display flex
186
+ padding 1PX 5PX
187
+
188
+ .activity-tag-text
189
+ font-size $F_TAG_NORMAL
190
+ color #F21902
191
+ line-height 1
192
+
193
+ .spec-text
194
+ font-size $F_SPEC
195
+ color var(--textContent)
196
+ line-height 15PX
197
+ margin-top 5PX
198
+ lineClamp(1)
199
+
200
+ .customization-text
201
+ font-size $F_CUSTOM
202
+ color var(--textMuted)
203
+ line-height 15PX
204
+ margin-top 5PX
205
+ lineClamp(3)
206
+
207
+ .content-bottom
208
+ display flex
209
+ flex-direction row
210
+ align-items center
211
+ justify-content space-between
212
+ margin-top 5PX
213
+
214
+ .price-container
215
+ display flex
216
+ flex-direction row
217
+ align-items center
218
+
219
+ .price-row
220
+ display flex
221
+ flex-direction row
222
+ align-items baseline
223
+
224
+ .price-symbol
225
+ font-size $F_PRICE_SYM
226
+ color var(--textMoney)
227
+ font-weight bold
228
+ margin-right 1PX
229
+
230
+ .price-value
231
+ font-size $F_PRICE_VAL
232
+ color var(--textMoney)
233
+ font-weight bold
234
+
235
+ .original-price
236
+ font-size 11PX
237
+ color var(--textMuted)
238
+ text-decoration line-through
239
+ margin-left 5PX
240
+
241
+ .buy-count-value
242
+ font-size $F_BUY_COUNT
243
+ color var(--textContent)
244
+ margin-left 10PX
245
+
246
+ .delete-container
247
+ position absolute
248
+ top 16PX
249
+ right 11PX
250
+ padding 5PX
251
+ z-index 10
252
+
253
+ .delete-icon
254
+ color var(--textContent)
255
+
256
+ .card-footer
257
+ width 100%
258
+ margin-top 12PX
@@ -0,0 +1,413 @@
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]: {
174
+ color: 'var(--primary)',
175
+ text: labels.product_type?.real
176
+ },
177
+ [MALL_PRODUCT_TYPE_VIRTUAL]: {
178
+ color: '#E4B700',
179
+ text: labels.product_type?.virtual
180
+ },
181
+ [MALL_PRODUCT_TYPE_SERVICE]: {
182
+ color: '#03BE02',
183
+ text: labels.product_type?.service
184
+ },
185
+ }
186
+ const item = colorMap[productType]
187
+ if (!item) return null
188
+ return (
189
+ <View className={styles['product-tag-container']}>
190
+ <Tag
191
+ type='light-warning'
192
+ text={item.text}
193
+ className={styles['product-tag']}
194
+ style={{ backgroundColor: item.color, color: '#fff' }}
195
+ />
196
+ </View>
197
+ )
198
+ }, [productType, labels])
199
+
200
+ const deliveryTags = useMemo(() => {
201
+ if (isEmpty(deliveryTypes)) return null
202
+ return deliveryTypes.map(type => {
203
+ let tagProps: any = null
204
+ if (type === MALL_PRODUCT_DELIVERY_TYPE_DINE) {
205
+ tagProps = {
206
+ text: labels.delivery_type?.dine,
207
+ color: '#2db7f5',
208
+ bg: 'rgba(145, 213, 255, 0.16)'
209
+ }
210
+ }
211
+ else if (type === MALL_PRODUCT_DELIVERY_TYPE_SELF) {
212
+ tagProps = {
213
+ text: labels.delivery_type?.self_pickup,
214
+ color: '#FF3D15',
215
+ bg: 'rgba(255,61,21,0.16)'
216
+ }
217
+ }
218
+ else if (type === MALL_PRODUCT_DELIVERY_TYPE_EXPRESS || type === MALL_PRODUCT_DELIVERY_TYPE_SHOP) {
219
+ tagProps = {
220
+ text: labels.delivery_type?.delivery,
221
+ color: '#FF8901',
222
+ bg: 'rgba(255,137,1,0.16)'
223
+ }
224
+ }
225
+ if (!tagProps) return null
226
+ return (
227
+ <Tag
228
+ key={type}
229
+ text={tagProps.text}
230
+ style={{ color: tagProps.color, backgroundColor: tagProps.bg, borderColor: tagProps.color }}
231
+ className={styles['delivery-tag']}
232
+ />
233
+ )
234
+ })
235
+ }, [deliveryTypes, labels])
236
+
237
+ const stockAndSaleElem = useMemo(() => {
238
+ if (!stockCount && !saleCount) return null
239
+ return (
240
+ <View className={styles['stock-and-sale']}>
241
+ {stockCount && (
242
+ <View className={styles['stock-box']}>
243
+ <Text className={styles['info-text']}>{labels.stock_label}</Text>
244
+ <Text className={styles['stock-text']}>{stockCount}</Text>
245
+ </View>
246
+ )}
247
+ {saleCount && (
248
+ <Text className={styles['info-text']}>
249
+ {labels.sale_label} {saleCount}
250
+ </Text>
251
+ )}
252
+ </View>
253
+ )
254
+ }, [stockCount, saleCount, labels])
255
+
256
+ return (
257
+ <View className={formatClassNames(styles['item-container'], className)} onClick={handleCardClick}>
258
+ <View className={styles['card-main-body']}>
259
+ {showCheckbox && (
260
+ <View className={styles['checkbox-container']}>
261
+ <Checkbox
262
+ checked={checkboxChecked}
263
+ disabled={checkboxDisabled}
264
+ readOnly
265
+ />
266
+ </View>
267
+ )}
268
+ <View className={styles['image-container']}>
269
+ <Image
270
+ src={image?.url}
271
+ className={styles['product-image']}
272
+ mode='aspectFill'
273
+ />
274
+ {(showSoldOut || showOffline) && (
275
+ <View className={styles['status-overlay']}>
276
+ <Text className={styles['status-text']}>
277
+ {showSoldOut ? labels.sold_out : labels.offline}
278
+ </Text>
279
+ </View>
280
+ )}
281
+ </View>
282
+ <View className={styles['content-container']}>
283
+ <View className={styles['content-body']}>
284
+ <View className={styles['title-row']} style={{ marginRight: showDelete ? '30PX' : '0' }}>
285
+ {productTypeTag}
286
+ <Text className={styles['title']} style={{ WebkitLineClamp: titleMaxLines }}>{title}</Text>
287
+ </View>
288
+ {(showMultipleSpec || stockCount || saleCount || barcode) && (
289
+ <ScrollView scrollX className={styles['second-row']} showScrollbar={false}>
290
+ <View className={styles['second-row-inner']}>
291
+ {showMultipleSpec && <Text className={styles['primary-text']}>{labels.multiple_spec}</Text>}
292
+ {showMultipleSpec && stockAndSaleElem && <View className={styles['info-line']} />}
293
+ {stockAndSaleElem}
294
+ {((showMultipleSpec || stockAndSaleElem) && barcode) && <View className={styles['info-line']} />}
295
+ {barcode && <Text className={styles['info-text']}>{labels.barcode_label} {barcode}</Text>}
296
+ </View>
297
+ </ScrollView>
298
+ )}
299
+ {(activityType || deliveryTags) && (
300
+ <View className={styles['tag-row']}>
301
+ {activityType && labels.activity_type && (
302
+ <View className={styles['activity-tag']}>
303
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && <View className={styles['activity-tag-badge']}><Text className={styles['activity-tag-badge-text']}>{labels.present_badge}</Text></View>}
304
+ <View className={styles['activity-tag-content']}>
305
+ <Text className={styles['activity-tag-text']}>
306
+ {activityType === PROMOTION_ACTIVITY_TYPE_FLASH && labels.activity_type.flash}
307
+ {activityType === PROMOTION_ACTIVITY_TYPE_BRAND && labels.activity_type.brand}
308
+ {activityType === PROMOTION_ACTIVITY_TYPE_HOT && labels.activity_type.hot}
309
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && labels.activity_type.present}
310
+ </Text>
311
+ </View>
312
+ </View>
313
+ )}
314
+ {deliveryTags}
315
+ </View>
316
+ )}
317
+ {
318
+ spec
319
+ ? (
320
+ <Text
321
+ className={styles['spec-text']}
322
+ style={{ WebkitLineClamp: specMaxLines }}
323
+ >
324
+ {labels.spec_label}:{spec}
325
+ </Text>
326
+ )
327
+ : undefined
328
+ }
329
+ {
330
+ customization
331
+ ? (
332
+ <Text
333
+ className={styles['customization-text']}
334
+ style={{ WebkitLineClamp: customizationMaxLines }}
335
+ >
336
+ {customization}
337
+ </Text>
338
+ )
339
+ : undefined
340
+ }
341
+ </View>
342
+ <View className={styles['content-bottom']}>
343
+ <View className={styles['price-container']}>
344
+ <View className={styles['price-row']}>
345
+ <Text className={styles['price-symbol']}>
346
+ ¥
347
+ </Text>
348
+ <Text className={styles['price-value']}>
349
+ {salePrice}
350
+ </Text>
351
+ </View>
352
+ {
353
+ originalPrice
354
+ ? (
355
+ <Text className={styles['original-price']}>
356
+ ¥{originalPrice}
357
+ </Text>
358
+ )
359
+ : undefined
360
+ }
361
+ </View>
362
+ <View
363
+ className={styles['action-area']}
364
+ >
365
+ {
366
+ buyCount
367
+ ? (
368
+ <Text className={styles['buy-count-value']}>
369
+ x{buyCount}
370
+ </Text>
371
+ )
372
+ : action
373
+ }
374
+ </View>
375
+ </View>
376
+ </View>
377
+ </View>
378
+ {
379
+ children
380
+ ? (
381
+ <View
382
+ className={styles['card-footer']}
383
+ onClick={e => e.stopPropagation()}
384
+ >
385
+ {children}
386
+ </View>
387
+ )
388
+ : undefined
389
+ }
390
+ {
391
+ showDelete
392
+ ? (
393
+ <View
394
+ className={styles['delete-container']}
395
+ onClick={(e: ITouchEvent) => {
396
+ e.stopPropagation()
397
+ onDelete?.()
398
+ }}
399
+ >
400
+ <Icon
401
+ name='trash'
402
+ size={13}
403
+ className={styles['delete-icon']}
404
+ />
405
+ </View>
406
+ )
407
+ : undefined
408
+ }
409
+ </View>
410
+ )
411
+ }
412
+
413
+ 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