@yorkjs/hive-ui 0.0.1 → 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.
@@ -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