@yorkjs/hive-ui 0.0.7 → 0.0.9

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@yorkjs/hive-ui",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Hive UI - Taro React component library",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from 'react'
2
2
  import { View, Text } from '@tarojs/components'
3
3
 
4
+ import { useTheme } from '../../config'
4
5
  import { isAndroid, isApp } from '../../util/env'
5
6
  import { formatClassNames } from '../../util/function'
6
7
 
@@ -18,8 +19,6 @@ export interface CheckboxProps {
18
19
  className?: string
19
20
  }
20
21
 
21
- const isDark = false
22
-
23
22
  const Checkbox: React.FC<CheckboxProps> = (props) => {
24
23
  const {
25
24
  label,
@@ -31,6 +30,8 @@ const Checkbox: React.FC<CheckboxProps> = (props) => {
31
30
  className,
32
31
  } = props
33
32
 
33
+ const { isDark } = useTheme()
34
+
34
35
  const handlePress = (e: any) => {
35
36
  if (disabled || readOnly) return
36
37
  onChange?.(!checked)
@@ -1,6 +1,7 @@
1
1
  import React, { useMemo } from 'react'
2
2
  import { View, Text } from '@tarojs/components'
3
3
 
4
+ import { useTheme } from '../../config'
4
5
  import { formatClassNames } from '../../util/function'
5
6
 
6
7
  import styles from './index.module.styl'
@@ -30,7 +31,7 @@ const Tag: React.FC<TagProps> = ({
30
31
  className,
31
32
  style
32
33
  }) => {
33
- const themeSelect = (light: string, dark: string) => (isDark ? dark : light)
34
+ const { themeSelect } = useTheme()
34
35
 
35
36
  const tagStyle = useMemo(() => {
36
37
  const colors = {
package/src/config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { useState, useEffect } from 'react'
2
+
3
+ let globalIsDark = false
4
+ const listeners = new Set<(isDark: boolean) => void>()
5
+
6
+ export const setGlobalTheme = (theme: 'light' | 'dark' | string) => {
7
+ globalIsDark = theme === 'dark'
8
+ listeners.forEach((callback) => callback(globalIsDark))
9
+ }
10
+
11
+ export const useTheme = () => {
12
+ const [isDark, setIsDark] = useState(globalIsDark)
13
+
14
+ useEffect(() => {
15
+ listeners.add(setIsDark)
16
+ return () => {
17
+ listeners.delete(setIsDark)
18
+ }
19
+ }, [])
20
+
21
+ const themeSelect = <T>(lightValue: T, darkValue: T): T => {
22
+ return isDark ? darkValue : lightValue
23
+ }
24
+
25
+ return { isDark, themeSelect }
26
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { setGlobalTheme, useTheme } from './config'
2
+
1
3
  export { default as NumberKeyboard } from './components/NumberKeyboard'
2
4
  export { default as Cell, setCellConfig } from './components/Cell'
3
5
  export { default as Icon } from './components/Icon'
@@ -5,4 +7,4 @@ export { default as Checkbox } from './components/Checkbox'
5
7
  export { default as Tag } from './components/Tag'
6
8
 
7
9
  export { default as FlowItem } from './item/FlowItem'
8
- export { default as ProductItem } from './item/ProductItem'
10
+ export { default as ProductItem, setProductItemConfig } from './item/ProductItem'
@@ -0,0 +1,440 @@
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 {
11
+ MALL_PRODUCT_TYPE_REAL,
12
+ MALL_PRODUCT_TYPE_VIRTUAL,
13
+ MALL_PRODUCT_TYPE_SERVICE,
14
+ MALL_PRODUCT_DELIVERY_TYPE_EXPRESS,
15
+ MALL_PRODUCT_DELIVERY_TYPE_SELF,
16
+ MALL_PRODUCT_DELIVERY_TYPE_SHOP,
17
+ MALL_PRODUCT_DELIVERY_TYPE_DINE,
18
+ PROMOTION_ACTIVITY_TYPE_FLASH,
19
+ PROMOTION_ACTIVITY_TYPE_BRAND,
20
+ PROMOTION_ACTIVITY_TYPE_HOT,
21
+ PROMOTION_ACTIVITY_TYPE_PRESENT,
22
+ } from '../../constant/mall'
23
+ import { getProductItemConfig } from './config'
24
+
25
+ import styles from './index.module.styl'
26
+
27
+ export interface IProductCardLabels {
28
+ product_type?: { real: string; virtual: string; service: string }
29
+ delivery_type?: { dine: string; self_pickup: string; delivery: string }
30
+ activity_type?: { flash: string; brand: string; hot: string; present: string }
31
+ present_badge?: string
32
+ stock_label?: string
33
+ sale_label?: string
34
+ multiple_spec?: string
35
+ barcode_label?: string
36
+ spec_label?: string
37
+ sold_out?: string
38
+ offline?: string
39
+ }
40
+
41
+ /** 商品卡片组件属性 */
42
+ export interface IProductCardProps {
43
+ /** 外部样式类 */
44
+ className?: string
45
+ /** 商品图片对象 */
46
+ image: { url: string }
47
+ /** 是否显示售罄标记 */
48
+ showSoldOut?: boolean
49
+ /** 是否显示已下架标记 */
50
+ showOffline?: boolean
51
+ /** 商品标题 */
52
+ title: string
53
+ /** 标题最大显示行数 (默认1) */
54
+ titleMaxLines?: number
55
+ /** 商品类型:实物/虚拟/服务 */
56
+ productType?: number
57
+ /** 是否显示“多规格”标签 */
58
+ showMultipleSpec?: boolean
59
+ /** 库存数量文案 */
60
+ stockCount?: string | number
61
+ /** 销量文案 */
62
+ saleCount?: string | number
63
+ /** 商品条码 */
64
+ barcode?: string
65
+ /** 自定义标签 */
66
+ customTags?: React.ReactNode
67
+ /** 规格描述文案 (如: 红色, 大号) */
68
+ spec?: string
69
+ /** 规格描述最大显示行数 (默认1) */
70
+ specMaxLines?: number
71
+ /** 活动类型:秒杀/品牌/热点/满赠 */
72
+ activityType?: number
73
+ /** 配送方式集合:堂食/自提/外送 */
74
+ deliveryTypes?: number[]
75
+ /** 做法/定制化信息描述 */
76
+ customization?: string
77
+ /** 做法描述最大显示行数 (默认3) */
78
+ customizationMaxLines?: number
79
+ /** 套餐信息描述 */
80
+ combo? : string
81
+ /** 套餐描述最大显示行数 (默认3) */
82
+ comboMaxLines?: number
83
+ /** 售价 */
84
+ salePrice: string | number
85
+ /** 原价 (划线价) */
86
+ originalPrice?: string | number
87
+ /** 已购数量 (显示为 xN) */
88
+ buyCount?: number
89
+ /** 是否显示左侧勾选框 */
90
+ showCheckbox?: boolean
91
+ /** 勾选框是否选中 */
92
+ checkboxChecked?: boolean
93
+ /** 勾选框是否禁用 */
94
+ checkboxDisabled?: boolean
95
+ /** 点击勾选框或卡片触发 (当 checkbox 可用时) */
96
+ onCheckboxClick?: () => void
97
+ /** 是否显示右上角删除按钮 */
98
+ showDelete?: boolean
99
+ /** 点击删除按钮回调 */
100
+ onDelete?: () => void
101
+ /** 右下角自定义操作区域 (与 buyCount 互斥) */
102
+ action?: React.ReactNode
103
+ /** 是否显示底部分边框 */
104
+ showBottomBorder?: boolean
105
+ /** 是否显示底部分割线 */
106
+ showSeparator?: boolean
107
+ // 卡片点击事件
108
+ onClick?: () => void
109
+ children?: React.ReactNode
110
+ }
111
+
112
+ const ProductItem: React.FC<IProductCardProps> = (props) => {
113
+ const {
114
+ image,
115
+ title,
116
+ titleMaxLines = 1,
117
+ productType,
118
+ customTags,
119
+ showMultipleSpec,
120
+ stockCount,
121
+ saleCount,
122
+ barcode,
123
+ spec,
124
+ specMaxLines = 1,
125
+ activityType,
126
+ deliveryTypes = [],
127
+ customization,
128
+ customizationMaxLines = 3,
129
+ combo,
130
+ comboMaxLines = 3,
131
+ salePrice,
132
+ originalPrice,
133
+ buyCount,
134
+ showSoldOut = false,
135
+ showOffline = false,
136
+ showCheckbox = false,
137
+ checkboxChecked = false,
138
+ checkboxDisabled = false,
139
+ onCheckboxClick,
140
+ showDelete,
141
+ onDelete,
142
+ action,
143
+ className,
144
+ showBottomBorder = false,
145
+ showSeparator = false,
146
+ onClick,
147
+ children
148
+ } = props
149
+
150
+ const handleCardClick = (e: ITouchEvent) => {
151
+ if (showCheckbox) {
152
+ if (!checkboxDisabled) {
153
+ onCheckboxClick?.()
154
+ }
155
+ }
156
+ else {
157
+ onClick?.()
158
+ }
159
+ }
160
+
161
+ const labels = useMemo(() => {
162
+ const globalConfig = getProductItemConfig()
163
+ return {
164
+ ...globalConfig,
165
+ }
166
+ }, [])
167
+
168
+ const productTypeTag = useMemo(() => {
169
+ if (productType === undefined) return null
170
+ const colorMap: Record<number, { color: string; text: string | undefined }> = {
171
+ [MALL_PRODUCT_TYPE_REAL]: {
172
+ color: 'var(--primary)',
173
+ text: labels.product_type?.real
174
+ },
175
+ [MALL_PRODUCT_TYPE_VIRTUAL]: {
176
+ color: '#E4B700',
177
+ text: labels.product_type?.virtual
178
+ },
179
+ [MALL_PRODUCT_TYPE_SERVICE]: {
180
+ color: '#03BE02',
181
+ text: labels.product_type?.service
182
+ },
183
+ }
184
+ const item = colorMap[productType]
185
+ if (!item) return null
186
+ return (
187
+ <View className={styles['product-tag-container']}>
188
+ <Tag
189
+ type='light-warning'
190
+ text={item.text}
191
+ className={styles['product-tag']}
192
+ style={{ backgroundColor: item.color, color: '#fff' }}
193
+ />
194
+ </View>
195
+ )
196
+ }, [productType, labels])
197
+
198
+ const deliveryTags = useMemo(() => {
199
+ if (isEmpty(deliveryTypes)) return null
200
+ return deliveryTypes.map(type => {
201
+ let tagProps: any = null
202
+ if (type === MALL_PRODUCT_DELIVERY_TYPE_DINE) {
203
+ tagProps = {
204
+ text: labels.delivery_type?.dine,
205
+ color: '#2db7f5',
206
+ bg: 'rgba(145, 213, 255, 0.16)'
207
+ }
208
+ }
209
+ else if (type === MALL_PRODUCT_DELIVERY_TYPE_SELF) {
210
+ tagProps = {
211
+ text: labels.delivery_type?.self_pickup,
212
+ color: '#FF3D15',
213
+ bg: 'rgba(255,61,21,0.16)'
214
+ }
215
+ }
216
+ else if (type === MALL_PRODUCT_DELIVERY_TYPE_EXPRESS || type === MALL_PRODUCT_DELIVERY_TYPE_SHOP) {
217
+ tagProps = {
218
+ text: labels.delivery_type?.delivery,
219
+ color: '#FF8901',
220
+ bg: 'rgba(255,137,1,0.16)'
221
+ }
222
+ }
223
+ if (!tagProps) return null
224
+ return (
225
+ <Tag
226
+ key={type}
227
+ text={tagProps.text}
228
+ style={{ color: tagProps.color, backgroundColor: tagProps.bg, borderColor: tagProps.color }}
229
+ className={styles['delivery-tag']}
230
+ />
231
+ )
232
+ })
233
+ }, [deliveryTypes, labels])
234
+
235
+ const stockAndSaleElem = useMemo(() => {
236
+ if (!stockCount && !saleCount) return null
237
+ return (
238
+ <View className={styles['stock-and-sale']}>
239
+ {stockCount && (
240
+ <View className={styles['stock-box']}>
241
+ <Text className={styles['info-text']}>{labels.stock_label}</Text>
242
+ <Text className={styles['stock-text']}>{stockCount}</Text>
243
+ </View>
244
+ )}
245
+ {saleCount && (
246
+ <Text className={styles['info-text']}>
247
+ {labels.sale_label} {saleCount}
248
+ </Text>
249
+ )}
250
+ </View>
251
+ )
252
+ }, [stockCount, saleCount, labels])
253
+
254
+ return (
255
+ <View
256
+ className={formatClassNames(
257
+ styles['item-container'],
258
+ {
259
+ [styles['show-bottom-border']]: showBottomBorder,
260
+ },
261
+ className
262
+ )}
263
+ onClick={handleCardClick}
264
+ >
265
+ <View className={formatClassNames(
266
+ styles['card-info'],
267
+ {
268
+ [styles['show-separator']]: showSeparator
269
+ }
270
+ )}>
271
+ <View className={styles['card-main-body']}>
272
+ {showCheckbox && (
273
+ <View className={styles['checkbox-container']}>
274
+ <Checkbox
275
+ checked={checkboxChecked}
276
+ disabled={checkboxDisabled}
277
+ readOnly
278
+ />
279
+ </View>
280
+ )}
281
+ <View className={styles['image-container']}>
282
+ <Image
283
+ src={image?.url}
284
+ className={styles['product-image']}
285
+ mode='aspectFill'
286
+ />
287
+ {(showSoldOut || showOffline) && (
288
+ <View className={styles['status-overlay']}>
289
+ <Text className={styles['status-text']}>
290
+ {showSoldOut ? labels.sold_out : labels.offline}
291
+ </Text>
292
+ </View>
293
+ )}
294
+ </View>
295
+ <View className={styles['content-container']}>
296
+ <View className={styles['content-body']}>
297
+ <View className={styles['title-row']} style={{ marginRight: showDelete ? '30PX' : '0' }}>
298
+ {productTypeTag}
299
+ <Text className={styles['title']} style={{ WebkitLineClamp: titleMaxLines }}>{title}</Text>
300
+ </View>
301
+ {(showMultipleSpec || stockCount || saleCount || barcode) && (
302
+ <ScrollView scrollX className={styles['second-row']} showScrollbar={false}>
303
+ <View className={styles['second-row-inner']}>
304
+ {showMultipleSpec && <Text className={styles['primary-text']}>{labels.multiple_spec}</Text>}
305
+ {showMultipleSpec && stockAndSaleElem && <View className={styles['info-line']} />}
306
+ {stockAndSaleElem}
307
+ {((showMultipleSpec || stockAndSaleElem) && barcode) && <View className={styles['info-line']} />}
308
+ {barcode && <Text className={styles['info-text']}>{labels.barcode_label} {barcode}</Text>}
309
+ </View>
310
+ </ScrollView>
311
+ )}
312
+ {customTags}
313
+ {(activityType || deliveryTags) && (
314
+ <View className={styles['tag-row']}>
315
+ {activityType && labels.activity_type && (
316
+ <View className={styles['activity-tag']}>
317
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && <View className={styles['activity-tag-badge']}><Text className={styles['activity-tag-badge-text']}>{labels.present_badge}</Text></View>}
318
+ <View className={styles['activity-tag-content']}>
319
+ <Text className={styles['activity-tag-text']}>
320
+ {activityType === PROMOTION_ACTIVITY_TYPE_FLASH && labels.activity_type.flash}
321
+ {activityType === PROMOTION_ACTIVITY_TYPE_BRAND && labels.activity_type.brand}
322
+ {activityType === PROMOTION_ACTIVITY_TYPE_HOT && labels.activity_type.hot}
323
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && labels.activity_type.present}
324
+ </Text>
325
+ </View>
326
+ </View>
327
+ )}
328
+ {deliveryTags}
329
+ </View>
330
+ )}
331
+ {
332
+ spec
333
+ ? (
334
+ <Text
335
+ className={styles['spec-text']}
336
+ style={{ WebkitLineClamp: specMaxLines }}
337
+ >
338
+ {labels.spec_label}:{spec}
339
+ </Text>
340
+ )
341
+ : undefined
342
+ }
343
+ {
344
+ customization
345
+ ? (
346
+ <Text
347
+ className={styles['customization-text']}
348
+ style={{ WebkitLineClamp: customizationMaxLines }}
349
+ >
350
+ {customization}
351
+ </Text>
352
+ )
353
+ : undefined
354
+ }
355
+ {
356
+ combo
357
+ ? (
358
+ <Text
359
+ className={styles['customization-text']}
360
+ style={{ WebkitLineClamp: comboMaxLines }}
361
+ >
362
+ {combo}
363
+ </Text>
364
+ )
365
+ : undefined
366
+ }
367
+ </View>
368
+ <View className={styles['content-bottom']}>
369
+ <View className={styles['price-container']}>
370
+ <View className={styles['price-row']}>
371
+ <Text className={styles['price-symbol']}>
372
+ ¥
373
+ </Text>
374
+ <Text className={styles['price-value']}>
375
+ {salePrice}
376
+ </Text>
377
+ </View>
378
+ {
379
+ originalPrice
380
+ ? (
381
+ <Text className={styles['original-price']}>
382
+ ¥{originalPrice}
383
+ </Text>
384
+ )
385
+ : undefined
386
+ }
387
+ </View>
388
+ <View
389
+ className={styles['action-area']}
390
+ >
391
+ {
392
+ buyCount
393
+ ? (
394
+ <Text className={styles['buy-count-value']}>
395
+ x{buyCount}
396
+ </Text>
397
+ )
398
+ : action
399
+ }
400
+ </View>
401
+ </View>
402
+ </View>
403
+ </View>
404
+ {
405
+ children
406
+ ? (
407
+ <View
408
+ className={styles['card-footer']}
409
+ onClick={e => e.stopPropagation()}
410
+ >
411
+ {children}
412
+ </View>
413
+ )
414
+ : undefined
415
+ }
416
+ </View>
417
+ {
418
+ showDelete
419
+ ? (
420
+ <View
421
+ className={styles['delete-container']}
422
+ onClick={(e: ITouchEvent) => {
423
+ e.stopPropagation()
424
+ onDelete?.()
425
+ }}
426
+ >
427
+ <Icon
428
+ name='trash'
429
+ size={13}
430
+ className={styles['delete-icon']}
431
+ />
432
+ </View>
433
+ )
434
+ : undefined
435
+ }
436
+ </View>
437
+ )
438
+ }
439
+
440
+ export default React.memo(ProductItem)
@@ -0,0 +1,35 @@
1
+ import { IProductCardLabels } from './ProductItem'
2
+
3
+ // 初始默认文案
4
+ let globalLabels: IProductCardLabels = {
5
+ product_type: {
6
+ real: '实物',
7
+ virtual: '虚拟',
8
+ service: '服务'
9
+ },
10
+ delivery_type: {
11
+ dine: '堂食',
12
+ self_pickup: '到店自取',
13
+ delivery: '送货上门'
14
+ },
15
+ activity_type: {
16
+ flash: '秒杀活动',
17
+ brand: '品牌活动',
18
+ hot: '热点活动',
19
+ present: '买赠活动'
20
+ },
21
+ present_badge: '赠',
22
+ stock_label: '库存',
23
+ sale_label: '销量',
24
+ multiple_spec: '多规格',
25
+ barcode_label: '条码',
26
+ spec_label: '规格',
27
+ sold_out: '售罄',
28
+ offline: '已下架'
29
+ }
30
+
31
+ export const setProductItemConfig = (labels: IProductCardLabels) => {
32
+ globalLabels = { ...globalLabels, ...labels }
33
+ }
34
+
35
+ export const getProductItemConfig = () => globalLabels
@@ -1,453 +1,8 @@
1
- import React, { useMemo } from 'react'
2
- import { View, Text, ScrollView, Image, ITouchEvent } from '@tarojs/components'
1
+ import ProductItemMain from './ProductItem'
2
+ import { setProductItemConfig } from './config'
3
3
 
4
- import { formatClassNames, isEmpty } from '../../util/function'
4
+ export type { IProductCardLabels, IProductCardProps } from './ProductItem'
5
5
 
6
- import Tag from '../../components/Tag'
7
- import Icon from '../../components/Icon'
8
- import Checkbox from '../../components/Checkbox'
6
+ export { setProductItemConfig }
9
7
 
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
- combo? : string
78
- /** 套餐描述最大显示行数 (默认3) */
79
- comboMaxLines?: number
80
- /** 售价 */
81
- salePrice: string | number
82
- /** 原价 (划线价) */
83
- originalPrice?: string | number
84
- /** 已购数量 (显示为 xN) */
85
- buyCount?: number
86
- /** 是否显示左侧勾选框 */
87
- showCheckbox?: boolean
88
- /** 勾选框是否选中 */
89
- checkboxChecked?: boolean
90
- /** 勾选框是否禁用 */
91
- checkboxDisabled?: boolean
92
- /** 点击勾选框或卡片触发 (当 checkbox 可用时) */
93
- onCheckboxClick?: () => void
94
- /** 是否显示右上角删除按钮 */
95
- showDelete?: boolean
96
- /** 点击删除按钮回调 */
97
- onDelete?: () => void
98
- /** 右下角自定义操作区域 (与 buyCount 互斥) */
99
- action?: React.ReactNode
100
- /** 国际化标签注入 */
101
- labels?: IProductCardLabels
102
- /** 是否显示底部分边框 */
103
- showBottomBorder?: boolean
104
- /** 是否显示底部分割线 */
105
- showSeparator?: boolean
106
- // 卡片点击事件
107
- onClick?: () => void
108
- children?: React.ReactNode
109
- }
110
-
111
- const DEFAULT_LABELS: IProductCardLabels = {
112
- product_type: { real: '实物', virtual: '虚拟', service: '服务' },
113
- delivery_type: { dine: '堂食', self_pickup: '到店自取', delivery: '送货上门' },
114
- activity_type: { flash: '秒杀活动', brand: '品牌活动', hot: '热点活动', present: '买赠活动' },
115
- present_badge: '赠',
116
- stock_label: '库存',
117
- sale_label: '销量',
118
- multiple_spec: '多规格',
119
- barcode_label: '条码',
120
- spec_label: '规格',
121
- sold_out: '售罄',
122
- offline: '已下架'
123
- }
124
-
125
- const ProductItem: React.FC<IProductCardProps> = (props) => {
126
- const {
127
- image,
128
- title,
129
- titleMaxLines = 1,
130
- productType,
131
- showMultipleSpec,
132
- stockCount,
133
- saleCount,
134
- barcode,
135
- spec,
136
- specMaxLines = 1,
137
- activityType,
138
- deliveryTypes = [],
139
- customization,
140
- customizationMaxLines = 3,
141
- combo,
142
- comboMaxLines = 3,
143
- salePrice,
144
- originalPrice,
145
- buyCount,
146
- showSoldOut = false,
147
- showOffline = false,
148
- showCheckbox = false,
149
- checkboxChecked = false,
150
- checkboxDisabled = false,
151
- onCheckboxClick,
152
- showDelete,
153
- onDelete,
154
- action,
155
- className,
156
- showBottomBorder = false,
157
- showSeparator = false,
158
- labels: customLabels,
159
- onClick,
160
- children
161
- } = props
162
-
163
- const handleCardClick = (e: ITouchEvent) => {
164
- if (showCheckbox) {
165
- if (!checkboxDisabled) {
166
- onCheckboxClick?.()
167
- }
168
- }
169
- else {
170
- onClick?.()
171
- }
172
- }
173
-
174
- const labels = useMemo(() => ({
175
- ...DEFAULT_LABELS,
176
- ...customLabels,
177
- product_type: { ...DEFAULT_LABELS.product_type, ...customLabels?.product_type },
178
- delivery_type: { ...DEFAULT_LABELS.delivery_type, ...customLabels?.delivery_type },
179
- activity_type: { ...DEFAULT_LABELS.activity_type, ...customLabels?.activity_type }
180
- }), [customLabels])
181
-
182
- const productTypeTag = useMemo(() => {
183
- if (productType === undefined) return null
184
- const colorMap: Record<number, { color: string; text: string | undefined }> = {
185
- [MALL_PRODUCT_TYPE_REAL]: {
186
- color: 'var(--primary)',
187
- text: labels.product_type?.real
188
- },
189
- [MALL_PRODUCT_TYPE_VIRTUAL]: {
190
- color: '#E4B700',
191
- text: labels.product_type?.virtual
192
- },
193
- [MALL_PRODUCT_TYPE_SERVICE]: {
194
- color: '#03BE02',
195
- text: labels.product_type?.service
196
- },
197
- }
198
- const item = colorMap[productType]
199
- if (!item) return null
200
- return (
201
- <View className={styles['product-tag-container']}>
202
- <Tag
203
- type='light-warning'
204
- text={item.text}
205
- className={styles['product-tag']}
206
- style={{ backgroundColor: item.color, color: '#fff' }}
207
- />
208
- </View>
209
- )
210
- }, [productType, labels])
211
-
212
- const deliveryTags = useMemo(() => {
213
- if (isEmpty(deliveryTypes)) return null
214
- return deliveryTypes.map(type => {
215
- let tagProps: any = null
216
- if (type === MALL_PRODUCT_DELIVERY_TYPE_DINE) {
217
- tagProps = {
218
- text: labels.delivery_type?.dine,
219
- color: '#2db7f5',
220
- bg: 'rgba(145, 213, 255, 0.16)'
221
- }
222
- }
223
- else if (type === MALL_PRODUCT_DELIVERY_TYPE_SELF) {
224
- tagProps = {
225
- text: labels.delivery_type?.self_pickup,
226
- color: '#FF3D15',
227
- bg: 'rgba(255,61,21,0.16)'
228
- }
229
- }
230
- else if (type === MALL_PRODUCT_DELIVERY_TYPE_EXPRESS || type === MALL_PRODUCT_DELIVERY_TYPE_SHOP) {
231
- tagProps = {
232
- text: labels.delivery_type?.delivery,
233
- color: '#FF8901',
234
- bg: 'rgba(255,137,1,0.16)'
235
- }
236
- }
237
- if (!tagProps) return null
238
- return (
239
- <Tag
240
- key={type}
241
- text={tagProps.text}
242
- style={{ color: tagProps.color, backgroundColor: tagProps.bg, borderColor: tagProps.color }}
243
- className={styles['delivery-tag']}
244
- />
245
- )
246
- })
247
- }, [deliveryTypes, labels])
248
-
249
- const stockAndSaleElem = useMemo(() => {
250
- if (!stockCount && !saleCount) return null
251
- return (
252
- <View className={styles['stock-and-sale']}>
253
- {stockCount && (
254
- <View className={styles['stock-box']}>
255
- <Text className={styles['info-text']}>{labels.stock_label}</Text>
256
- <Text className={styles['stock-text']}>{stockCount}</Text>
257
- </View>
258
- )}
259
- {saleCount && (
260
- <Text className={styles['info-text']}>
261
- {labels.sale_label} {saleCount}
262
- </Text>
263
- )}
264
- </View>
265
- )
266
- }, [stockCount, saleCount, labels])
267
-
268
- return (
269
- <View
270
- className={formatClassNames(
271
- styles['item-container'],
272
- {
273
- [styles['show-bottom-border']]: showBottomBorder,
274
- },
275
- className
276
- )}
277
- onClick={handleCardClick}
278
- >
279
- <View className={formatClassNames(
280
- styles['card-info'],
281
- {
282
- [styles['show-separator']]: showSeparator
283
- }
284
- )}>
285
- <View className={styles['card-main-body']}>
286
- {showCheckbox && (
287
- <View className={styles['checkbox-container']}>
288
- <Checkbox
289
- checked={checkboxChecked}
290
- disabled={checkboxDisabled}
291
- readOnly
292
- />
293
- </View>
294
- )}
295
- <View className={styles['image-container']}>
296
- <Image
297
- src={image?.url}
298
- className={styles['product-image']}
299
- mode='aspectFill'
300
- />
301
- {(showSoldOut || showOffline) && (
302
- <View className={styles['status-overlay']}>
303
- <Text className={styles['status-text']}>
304
- {showSoldOut ? labels.sold_out : labels.offline}
305
- </Text>
306
- </View>
307
- )}
308
- </View>
309
- <View className={styles['content-container']}>
310
- <View className={styles['content-body']}>
311
- <View className={styles['title-row']} style={{ marginRight: showDelete ? '30PX' : '0' }}>
312
- {productTypeTag}
313
- <Text className={styles['title']} style={{ WebkitLineClamp: titleMaxLines }}>{title}</Text>
314
- </View>
315
- {(showMultipleSpec || stockCount || saleCount || barcode) && (
316
- <ScrollView scrollX className={styles['second-row']} showScrollbar={false}>
317
- <View className={styles['second-row-inner']}>
318
- {showMultipleSpec && <Text className={styles['primary-text']}>{labels.multiple_spec}</Text>}
319
- {showMultipleSpec && stockAndSaleElem && <View className={styles['info-line']} />}
320
- {stockAndSaleElem}
321
- {((showMultipleSpec || stockAndSaleElem) && barcode) && <View className={styles['info-line']} />}
322
- {barcode && <Text className={styles['info-text']}>{labels.barcode_label} {barcode}</Text>}
323
- </View>
324
- </ScrollView>
325
- )}
326
- {(activityType || deliveryTags) && (
327
- <View className={styles['tag-row']}>
328
- {activityType && labels.activity_type && (
329
- <View className={styles['activity-tag']}>
330
- {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && <View className={styles['activity-tag-badge']}><Text className={styles['activity-tag-badge-text']}>{labels.present_badge}</Text></View>}
331
- <View className={styles['activity-tag-content']}>
332
- <Text className={styles['activity-tag-text']}>
333
- {activityType === PROMOTION_ACTIVITY_TYPE_FLASH && labels.activity_type.flash}
334
- {activityType === PROMOTION_ACTIVITY_TYPE_BRAND && labels.activity_type.brand}
335
- {activityType === PROMOTION_ACTIVITY_TYPE_HOT && labels.activity_type.hot}
336
- {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && labels.activity_type.present}
337
- </Text>
338
- </View>
339
- </View>
340
- )}
341
- {deliveryTags}
342
- </View>
343
- )}
344
- {
345
- spec
346
- ? (
347
- <Text
348
- className={styles['spec-text']}
349
- style={{ WebkitLineClamp: specMaxLines }}
350
- >
351
- {labels.spec_label}:{spec}
352
- </Text>
353
- )
354
- : undefined
355
- }
356
- {
357
- customization
358
- ? (
359
- <Text
360
- className={styles['customization-text']}
361
- style={{ WebkitLineClamp: customizationMaxLines }}
362
- >
363
- {customization}
364
- </Text>
365
- )
366
- : undefined
367
- }
368
- {
369
- combo
370
- ? (
371
- <Text
372
- className={styles['customization-text']}
373
- style={{ WebkitLineClamp: comboMaxLines }}
374
- >
375
- {combo}
376
- </Text>
377
- )
378
- : undefined
379
- }
380
- </View>
381
- <View className={styles['content-bottom']}>
382
- <View className={styles['price-container']}>
383
- <View className={styles['price-row']}>
384
- <Text className={styles['price-symbol']}>
385
- ¥
386
- </Text>
387
- <Text className={styles['price-value']}>
388
- {salePrice}
389
- </Text>
390
- </View>
391
- {
392
- originalPrice
393
- ? (
394
- <Text className={styles['original-price']}>
395
- ¥{originalPrice}
396
- </Text>
397
- )
398
- : undefined
399
- }
400
- </View>
401
- <View
402
- className={styles['action-area']}
403
- >
404
- {
405
- buyCount
406
- ? (
407
- <Text className={styles['buy-count-value']}>
408
- x{buyCount}
409
- </Text>
410
- )
411
- : action
412
- }
413
- </View>
414
- </View>
415
- </View>
416
- </View>
417
- {
418
- children
419
- ? (
420
- <View
421
- className={styles['card-footer']}
422
- onClick={e => e.stopPropagation()}
423
- >
424
- {children}
425
- </View>
426
- )
427
- : undefined
428
- }
429
- </View>
430
- {
431
- showDelete
432
- ? (
433
- <View
434
- className={styles['delete-container']}
435
- onClick={(e: ITouchEvent) => {
436
- e.stopPropagation()
437
- onDelete?.()
438
- }}
439
- >
440
- <Icon
441
- name='trash'
442
- size={13}
443
- className={styles['delete-icon']}
444
- />
445
- </View>
446
- )
447
- : undefined
448
- }
449
- </View>
450
- )
451
- }
452
-
453
- export default React.memo(ProductItem)
8
+ export default ProductItemMain