@yorkjs/hive-ui 0.0.7 → 0.0.8

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.8",
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)
@@ -0,0 +1,60 @@
1
+ @import '../../styles/mixin.styl'
2
+
3
+ $CELL_PADDING_HORIZONTAL = 16PX
4
+ $CELL_PADDING_VERTICAL = 14PX
5
+ $TITLE_FONT_SIZE = 16PX
6
+
7
+ .field-card-container
8
+ border-radius 10PX
9
+ background-color var(--containerCard)
10
+ margin 0 12PX
11
+ margin-bottom 12PX
12
+ overflow visible
13
+
14
+ .card-header
15
+ display flex
16
+ flex-direction row
17
+ align-items center
18
+ justify-content space-between
19
+ padding $CELL_PADDING_VERTICAL $CELL_PADDING_HORIZONTAL
20
+ position relative
21
+ min-height 48PX
22
+ box-sizing border-box
23
+ halfBorder(bottom, var(--lineBorder))
24
+
25
+ .header-left
26
+ flex 1
27
+ display flex
28
+ flex-direction row
29
+ align-items center
30
+ min-width 0
31
+
32
+ .header-title
33
+ color var(--textTitle)
34
+ font-size $TITLE_FONT_SIZE
35
+ font-weight 500
36
+ line-height 1.4
37
+ lineClamp(1)
38
+
39
+ .edit-icon
40
+ margin-left 6PX
41
+ color var(--textMuted)
42
+ flex-shrink 0
43
+
44
+ .header-extra
45
+ margin-left 12PX
46
+ flex-shrink 0
47
+ display flex
48
+ align-items center
49
+ // 动作区域通常字号稍小
50
+ font-size 14PX
51
+
52
+ .action-text
53
+ color #FF4D4F
54
+
55
+ .action-link
56
+ color var(--primary)
57
+
58
+ .card-content
59
+ border-radius 0 0 10PX 10PX
60
+ overflow hidden
@@ -0,0 +1,83 @@
1
+ import React from 'react'
2
+ import { View, Text, ITouchEvent } from '@tarojs/components'
3
+
4
+ import { formatClassNames } from '../../util/function'
5
+ import Icon from '../Icon'
6
+
7
+ import styles from './index.module.styl'
8
+
9
+ export interface FieldCardProps {
10
+ /** 标题内容 */
11
+ title: string
12
+ /** 是否显示标题旁的编辑小图标 */
13
+ showEditIcon?: boolean
14
+ /** 右侧动作区域:支持字符串、Icon 或自定义节点 */
15
+ extra?: React.ReactNode
16
+ /** 点击整个 Header 区域的回调 */
17
+ onHeaderClick?: (e: ITouchEvent) => void
18
+ /** 点击右侧 extra 区域的回调 (如果传了 extra 且需要独立点击) */
19
+ onExtraClick?: (e: ITouchEvent) => void
20
+ /** 子组件 */
21
+ children?: React.ReactNode
22
+ /** 外部样式 */
23
+ className?: string
24
+ }
25
+
26
+ const FieldCard: React.FC<FieldCardProps> = (props) => {
27
+ const {
28
+ title,
29
+ showEditIcon = false,
30
+ extra,
31
+ onHeaderClick,
32
+ onExtraClick,
33
+ children,
34
+ className
35
+ } = props
36
+
37
+ const handleExtraClick = (e: ITouchEvent) => {
38
+ if (onExtraClick) {
39
+ e.stopPropagation()
40
+ onExtraClick(e)
41
+ }
42
+ }
43
+
44
+ return (
45
+ <View className={formatClassNames(styles['field-card-container'], className)}>
46
+ <View className={styles['card-header']} onClick={onHeaderClick}>
47
+ <View className={styles['header-left']}>
48
+ <Text className={styles['header-title']}>{title}</Text>
49
+ {
50
+ showEditIcon
51
+ ? (
52
+ <Icon name='edit' size={14} className={styles['edit-icon']} />
53
+ )
54
+ : undefined
55
+ }
56
+ </View>
57
+
58
+ {extra && (
59
+ <View
60
+ className={styles['header-extra']}
61
+ onClick={handleExtraClick}
62
+ >
63
+ {
64
+ typeof extra === 'string'
65
+ ? (
66
+ <Text className={styles['action-text']}>{extra}</Text>
67
+ )
68
+ : (
69
+ extra
70
+ )
71
+ }
72
+ </View>
73
+ )}
74
+ </View>
75
+
76
+ <View className={styles['card-content']}>
77
+ {children}
78
+ </View>
79
+ </View>
80
+ )
81
+ }
82
+
83
+ export default FieldCard
@@ -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,436 @@
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
+ spec?: string
67
+ /** 规格描述最大显示行数 (默认1) */
68
+ specMaxLines?: number
69
+ /** 活动类型:秒杀/品牌/热点/满赠 */
70
+ activityType?: number
71
+ /** 配送方式集合:堂食/自提/外送 */
72
+ deliveryTypes?: number[]
73
+ /** 做法/定制化信息描述 */
74
+ customization?: string
75
+ /** 做法描述最大显示行数 (默认3) */
76
+ customizationMaxLines?: number
77
+ /** 套餐信息描述 */
78
+ combo? : string
79
+ /** 套餐描述最大显示行数 (默认3) */
80
+ comboMaxLines?: number
81
+ /** 售价 */
82
+ salePrice: string | number
83
+ /** 原价 (划线价) */
84
+ originalPrice?: string | number
85
+ /** 已购数量 (显示为 xN) */
86
+ buyCount?: number
87
+ /** 是否显示左侧勾选框 */
88
+ showCheckbox?: boolean
89
+ /** 勾选框是否选中 */
90
+ checkboxChecked?: boolean
91
+ /** 勾选框是否禁用 */
92
+ checkboxDisabled?: boolean
93
+ /** 点击勾选框或卡片触发 (当 checkbox 可用时) */
94
+ onCheckboxClick?: () => void
95
+ /** 是否显示右上角删除按钮 */
96
+ showDelete?: boolean
97
+ /** 点击删除按钮回调 */
98
+ onDelete?: () => void
99
+ /** 右下角自定义操作区域 (与 buyCount 互斥) */
100
+ action?: React.ReactNode
101
+ /** 是否显示底部分边框 */
102
+ showBottomBorder?: boolean
103
+ /** 是否显示底部分割线 */
104
+ showSeparator?: boolean
105
+ // 卡片点击事件
106
+ onClick?: () => void
107
+ children?: React.ReactNode
108
+ }
109
+
110
+ const ProductItem: React.FC<IProductCardProps> = (props) => {
111
+ const {
112
+ image,
113
+ title,
114
+ titleMaxLines = 1,
115
+ productType,
116
+ showMultipleSpec,
117
+ stockCount,
118
+ saleCount,
119
+ barcode,
120
+ spec,
121
+ specMaxLines = 1,
122
+ activityType,
123
+ deliveryTypes = [],
124
+ customization,
125
+ customizationMaxLines = 3,
126
+ combo,
127
+ comboMaxLines = 3,
128
+ salePrice,
129
+ originalPrice,
130
+ buyCount,
131
+ showSoldOut = false,
132
+ showOffline = false,
133
+ showCheckbox = false,
134
+ checkboxChecked = false,
135
+ checkboxDisabled = false,
136
+ onCheckboxClick,
137
+ showDelete,
138
+ onDelete,
139
+ action,
140
+ className,
141
+ showBottomBorder = false,
142
+ showSeparator = false,
143
+ onClick,
144
+ children
145
+ } = props
146
+
147
+ const handleCardClick = (e: ITouchEvent) => {
148
+ if (showCheckbox) {
149
+ if (!checkboxDisabled) {
150
+ onCheckboxClick?.()
151
+ }
152
+ }
153
+ else {
154
+ onClick?.()
155
+ }
156
+ }
157
+
158
+ const labels = useMemo(() => {
159
+ const globalConfig = getProductItemConfig()
160
+ return {
161
+ ...globalConfig,
162
+ }
163
+ }, [])
164
+
165
+ const productTypeTag = useMemo(() => {
166
+ if (productType === undefined) return null
167
+ const colorMap: Record<number, { color: string; text: string | undefined }> = {
168
+ [MALL_PRODUCT_TYPE_REAL]: {
169
+ color: 'var(--primary)',
170
+ text: labels.product_type?.real
171
+ },
172
+ [MALL_PRODUCT_TYPE_VIRTUAL]: {
173
+ color: '#E4B700',
174
+ text: labels.product_type?.virtual
175
+ },
176
+ [MALL_PRODUCT_TYPE_SERVICE]: {
177
+ color: '#03BE02',
178
+ text: labels.product_type?.service
179
+ },
180
+ }
181
+ const item = colorMap[productType]
182
+ if (!item) return null
183
+ return (
184
+ <View className={styles['product-tag-container']}>
185
+ <Tag
186
+ type='light-warning'
187
+ text={item.text}
188
+ className={styles['product-tag']}
189
+ style={{ backgroundColor: item.color, color: '#fff' }}
190
+ />
191
+ </View>
192
+ )
193
+ }, [productType, labels])
194
+
195
+ const deliveryTags = useMemo(() => {
196
+ if (isEmpty(deliveryTypes)) return null
197
+ return deliveryTypes.map(type => {
198
+ let tagProps: any = null
199
+ if (type === MALL_PRODUCT_DELIVERY_TYPE_DINE) {
200
+ tagProps = {
201
+ text: labels.delivery_type?.dine,
202
+ color: '#2db7f5',
203
+ bg: 'rgba(145, 213, 255, 0.16)'
204
+ }
205
+ }
206
+ else if (type === MALL_PRODUCT_DELIVERY_TYPE_SELF) {
207
+ tagProps = {
208
+ text: labels.delivery_type?.self_pickup,
209
+ color: '#FF3D15',
210
+ bg: 'rgba(255,61,21,0.16)'
211
+ }
212
+ }
213
+ else if (type === MALL_PRODUCT_DELIVERY_TYPE_EXPRESS || type === MALL_PRODUCT_DELIVERY_TYPE_SHOP) {
214
+ tagProps = {
215
+ text: labels.delivery_type?.delivery,
216
+ color: '#FF8901',
217
+ bg: 'rgba(255,137,1,0.16)'
218
+ }
219
+ }
220
+ if (!tagProps) return null
221
+ return (
222
+ <Tag
223
+ key={type}
224
+ text={tagProps.text}
225
+ style={{ color: tagProps.color, backgroundColor: tagProps.bg, borderColor: tagProps.color }}
226
+ className={styles['delivery-tag']}
227
+ />
228
+ )
229
+ })
230
+ }, [deliveryTypes, labels])
231
+
232
+ const stockAndSaleElem = useMemo(() => {
233
+ if (!stockCount && !saleCount) return null
234
+ return (
235
+ <View className={styles['stock-and-sale']}>
236
+ {stockCount && (
237
+ <View className={styles['stock-box']}>
238
+ <Text className={styles['info-text']}>{labels.stock_label}</Text>
239
+ <Text className={styles['stock-text']}>{stockCount}</Text>
240
+ </View>
241
+ )}
242
+ {saleCount && (
243
+ <Text className={styles['info-text']}>
244
+ {labels.sale_label} {saleCount}
245
+ </Text>
246
+ )}
247
+ </View>
248
+ )
249
+ }, [stockCount, saleCount, labels])
250
+
251
+ return (
252
+ <View
253
+ className={formatClassNames(
254
+ styles['item-container'],
255
+ {
256
+ [styles['show-bottom-border']]: showBottomBorder,
257
+ },
258
+ className
259
+ )}
260
+ onClick={handleCardClick}
261
+ >
262
+ <View className={formatClassNames(
263
+ styles['card-info'],
264
+ {
265
+ [styles['show-separator']]: showSeparator
266
+ }
267
+ )}>
268
+ <View className={styles['card-main-body']}>
269
+ {showCheckbox && (
270
+ <View className={styles['checkbox-container']}>
271
+ <Checkbox
272
+ checked={checkboxChecked}
273
+ disabled={checkboxDisabled}
274
+ readOnly
275
+ />
276
+ </View>
277
+ )}
278
+ <View className={styles['image-container']}>
279
+ <Image
280
+ src={image?.url}
281
+ className={styles['product-image']}
282
+ mode='aspectFill'
283
+ />
284
+ {(showSoldOut || showOffline) && (
285
+ <View className={styles['status-overlay']}>
286
+ <Text className={styles['status-text']}>
287
+ {showSoldOut ? labels.sold_out : labels.offline}
288
+ </Text>
289
+ </View>
290
+ )}
291
+ </View>
292
+ <View className={styles['content-container']}>
293
+ <View className={styles['content-body']}>
294
+ <View className={styles['title-row']} style={{ marginRight: showDelete ? '30PX' : '0' }}>
295
+ {productTypeTag}
296
+ <Text className={styles['title']} style={{ WebkitLineClamp: titleMaxLines }}>{title}</Text>
297
+ </View>
298
+ {(showMultipleSpec || stockCount || saleCount || barcode) && (
299
+ <ScrollView scrollX className={styles['second-row']} showScrollbar={false}>
300
+ <View className={styles['second-row-inner']}>
301
+ {showMultipleSpec && <Text className={styles['primary-text']}>{labels.multiple_spec}</Text>}
302
+ {showMultipleSpec && stockAndSaleElem && <View className={styles['info-line']} />}
303
+ {stockAndSaleElem}
304
+ {((showMultipleSpec || stockAndSaleElem) && barcode) && <View className={styles['info-line']} />}
305
+ {barcode && <Text className={styles['info-text']}>{labels.barcode_label} {barcode}</Text>}
306
+ </View>
307
+ </ScrollView>
308
+ )}
309
+ {(activityType || deliveryTags) && (
310
+ <View className={styles['tag-row']}>
311
+ {activityType && labels.activity_type && (
312
+ <View className={styles['activity-tag']}>
313
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && <View className={styles['activity-tag-badge']}><Text className={styles['activity-tag-badge-text']}>{labels.present_badge}</Text></View>}
314
+ <View className={styles['activity-tag-content']}>
315
+ <Text className={styles['activity-tag-text']}>
316
+ {activityType === PROMOTION_ACTIVITY_TYPE_FLASH && labels.activity_type.flash}
317
+ {activityType === PROMOTION_ACTIVITY_TYPE_BRAND && labels.activity_type.brand}
318
+ {activityType === PROMOTION_ACTIVITY_TYPE_HOT && labels.activity_type.hot}
319
+ {activityType === PROMOTION_ACTIVITY_TYPE_PRESENT && labels.activity_type.present}
320
+ </Text>
321
+ </View>
322
+ </View>
323
+ )}
324
+ {deliveryTags}
325
+ </View>
326
+ )}
327
+ {
328
+ spec
329
+ ? (
330
+ <Text
331
+ className={styles['spec-text']}
332
+ style={{ WebkitLineClamp: specMaxLines }}
333
+ >
334
+ {labels.spec_label}:{spec}
335
+ </Text>
336
+ )
337
+ : undefined
338
+ }
339
+ {
340
+ customization
341
+ ? (
342
+ <Text
343
+ className={styles['customization-text']}
344
+ style={{ WebkitLineClamp: customizationMaxLines }}
345
+ >
346
+ {customization}
347
+ </Text>
348
+ )
349
+ : undefined
350
+ }
351
+ {
352
+ combo
353
+ ? (
354
+ <Text
355
+ className={styles['customization-text']}
356
+ style={{ WebkitLineClamp: comboMaxLines }}
357
+ >
358
+ {combo}
359
+ </Text>
360
+ )
361
+ : undefined
362
+ }
363
+ </View>
364
+ <View className={styles['content-bottom']}>
365
+ <View className={styles['price-container']}>
366
+ <View className={styles['price-row']}>
367
+ <Text className={styles['price-symbol']}>
368
+ ¥
369
+ </Text>
370
+ <Text className={styles['price-value']}>
371
+ {salePrice}
372
+ </Text>
373
+ </View>
374
+ {
375
+ originalPrice
376
+ ? (
377
+ <Text className={styles['original-price']}>
378
+ ¥{originalPrice}
379
+ </Text>
380
+ )
381
+ : undefined
382
+ }
383
+ </View>
384
+ <View
385
+ className={styles['action-area']}
386
+ >
387
+ {
388
+ buyCount
389
+ ? (
390
+ <Text className={styles['buy-count-value']}>
391
+ x{buyCount}
392
+ </Text>
393
+ )
394
+ : action
395
+ }
396
+ </View>
397
+ </View>
398
+ </View>
399
+ </View>
400
+ {
401
+ children
402
+ ? (
403
+ <View
404
+ className={styles['card-footer']}
405
+ onClick={e => e.stopPropagation()}
406
+ >
407
+ {children}
408
+ </View>
409
+ )
410
+ : undefined
411
+ }
412
+ </View>
413
+ {
414
+ showDelete
415
+ ? (
416
+ <View
417
+ className={styles['delete-container']}
418
+ onClick={(e: ITouchEvent) => {
419
+ e.stopPropagation()
420
+ onDelete?.()
421
+ }}
422
+ >
423
+ <Icon
424
+ name='trash'
425
+ size={13}
426
+ className={styles['delete-icon']}
427
+ />
428
+ </View>
429
+ )
430
+ : undefined
431
+ }
432
+ </View>
433
+ )
434
+ }
435
+
436
+ 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