@anker-in/campaign-ui 0.5.3 → 0.5.4-beta.1

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.
Files changed (26) hide show
  1. package/dist/cjs/components/credits/creditsFaq/index.js +1 -1
  2. package/dist/cjs/components/credits/creditsFaq/index.js.map +3 -3
  3. package/dist/cjs/components/credits/creditsMemberPrice/CreditsMemberPrice.js +1 -1
  4. package/dist/cjs/components/credits/creditsMemberPrice/CreditsMemberPrice.js.map +2 -2
  5. package/dist/cjs/components/credits/creditsRedeemList/CreditsRedeemList.js +1 -1
  6. package/dist/cjs/components/credits/creditsRedeemList/CreditsRedeemList.js.map +3 -3
  7. package/dist/cjs/components/credits/modal/ActivitiesModal.js +1 -1
  8. package/dist/cjs/components/credits/modal/ActivitiesModal.js.map +3 -3
  9. package/dist/cjs/components/credits/modal/RewardsModal.js +1 -1
  10. package/dist/cjs/components/credits/modal/RewardsModal.js.map +3 -3
  11. package/dist/esm/components/credits/creditsFaq/index.js +1 -1
  12. package/dist/esm/components/credits/creditsFaq/index.js.map +3 -3
  13. package/dist/esm/components/credits/creditsMemberPrice/CreditsMemberPrice.js +1 -1
  14. package/dist/esm/components/credits/creditsMemberPrice/CreditsMemberPrice.js.map +3 -3
  15. package/dist/esm/components/credits/creditsRedeemList/CreditsRedeemList.js +1 -1
  16. package/dist/esm/components/credits/creditsRedeemList/CreditsRedeemList.js.map +3 -3
  17. package/dist/esm/components/credits/modal/ActivitiesModal.js +1 -1
  18. package/dist/esm/components/credits/modal/ActivitiesModal.js.map +3 -3
  19. package/dist/esm/components/credits/modal/RewardsModal.js +1 -1
  20. package/dist/esm/components/credits/modal/RewardsModal.js.map +3 -3
  21. package/package.json +1 -1
  22. package/src/components/credits/creditsFaq/index.tsx +47 -42
  23. package/src/components/credits/creditsMemberPrice/CreditsMemberPrice.tsx +35 -42
  24. package/src/components/credits/creditsRedeemList/CreditsRedeemList.tsx +66 -58
  25. package/src/components/credits/modal/ActivitiesModal.tsx +62 -56
  26. package/src/components/credits/modal/RewardsModal.tsx +43 -30
@@ -1,4 +1,4 @@
1
- import { Container, Heading, Tabs, TabsList, TabsTrigger } from '@anker-in/headless-ui'
1
+ import { Container, Heading, Tabs, TabsContent, TabsList, TabsTrigger } from '@anker-in/headless-ui'
2
2
  import classNames from 'classnames'
3
3
  import { useCallback, useMemo, useState } from 'react'
4
4
 
@@ -32,8 +32,6 @@ export const CreditsRedeemList = ({ copy, id }: { copy: CreditsRedeemListCopy; i
32
32
 
33
33
  const isLogin = Object.keys(profile || {}).length > 0
34
34
 
35
- const [activeTab, setActiveTab] = useState<string>(copy.list?.[0]?.label || '')
36
-
37
35
  const [popRedeemData, setPopRedeemData] = useState<RedeemableItemType | undefined>(undefined)
38
36
 
39
37
  const [rules, setRules] = useState<string | string[]>()
@@ -69,38 +67,43 @@ export const CreditsRedeemList = ({ copy, id }: { copy: CreditsRedeemListCopy; i
69
67
  image: item.goods_url,
70
68
  }
71
69
  })
72
- }, [copy.list, activeTab, redeemableList])
70
+ }, [redeemableList])
73
71
 
74
72
  const handles = alpcList.map(item => item.handle)
75
73
  const { data: products } = useProductsByHandles({ handles })
76
74
 
77
- const list = useMemo(() => {
78
- const currentList = copy.list?.find(item => item.label === activeTab)
79
- return (currentList?.list || [])
80
- .filter(item => alpcList.some(alpcItem => String(alpcItem.id) === String(item.id)))
81
- .map(item => {
82
- const alpcItem = alpcList.find(alpcItem => String(alpcItem.id) === String(item.id))
83
- const product = products?.find(product => product.handle === alpcItem?.handle)
84
- const variant = product?.variants.find(variant => variant.sku === alpcItem?.sku)
85
- // 过滤掉库存不足的商品
86
- if (alpcItem?.isLimited && (!alpcItem?.remainingInventory || alpcItem.remainingInventory <= 0)) return null
87
- if (alpcItem?.consumeType === AlpcConsumeType.Product && (!product || !variant)) return null
88
-
89
- // 检查 GoGift 是否已经兑换过(只有 GoGift 限制兑换一次)
90
- const hasRedeemed = item.type === ConsumeType.GoGift &&
91
- isLogin &&
92
- myRewards.some(reward => reward.rule_id === Number(item.id))
93
-
94
- return {
95
- alpc: alpcList.find(alpcItem => String(alpcItem.id) === String(item.id)),
96
- config: item,
97
- product,
98
- variant,
99
- hasRedeemed,
100
- }
101
- })
102
- .filter(item => item !== null) as unknown as RedeemableItemType[]
103
- }, [alpcList, products, isLogin, myRewards])
75
+ // tab label 分组预计算列表,配合 TabsContent 声明式渲染,避免手动 activeTab 字符串比较
76
+ const listByTab = useMemo(() => {
77
+ const map = new Map<string, RedeemableItemType[]>()
78
+ ;(copy.list || []).forEach(categoryItem => {
79
+ const items = (categoryItem.list || [])
80
+ .filter(item => alpcList.some(alpcItem => String(alpcItem.id) === String(item.id)))
81
+ .map(item => {
82
+ const alpcItem = alpcList.find(alpcItem => String(alpcItem.id) === String(item.id))
83
+ const product = products?.find(product => product.handle === alpcItem?.handle)
84
+ const variant = product?.variants.find(variant => variant.sku === alpcItem?.sku)
85
+ // 过滤掉库存不足的商品
86
+ if (alpcItem?.isLimited && (!alpcItem?.remainingInventory || alpcItem.remainingInventory <= 0)) return null
87
+ if (alpcItem?.consumeType === AlpcConsumeType.Product && (!product || !variant)) return null
88
+
89
+ // 检查 GoGift 是否已经兑换过(只有 GoGift 限制兑换一次)
90
+ const hasRedeemed = item.type === ConsumeType.GoGift &&
91
+ isLogin &&
92
+ myRewards.some(reward => reward.rule_id === Number(item.id))
93
+
94
+ return {
95
+ alpc: alpcItem,
96
+ config: item,
97
+ product,
98
+ variant,
99
+ hasRedeemed,
100
+ }
101
+ })
102
+ .filter(item => item !== null) as unknown as RedeemableItemType[]
103
+ map.set(categoryItem.label, items)
104
+ })
105
+ return map
106
+ }, [copy.list, alpcList, products, isLogin, myRewards])
104
107
 
105
108
  const handleRedeemError = useCallback(
106
109
  (errorCode: AlpcErrorCode) => {
@@ -142,9 +145,8 @@ export const CreditsRedeemList = ({ copy, id }: { copy: CreditsRedeemListCopy; i
142
145
  shape={rounded ? 'rounded' : 'square'}
143
146
  align="left"
144
147
  className={classNames('py-[24px] md:justify-center')}
145
- value={activeTab.toString()}
148
+ defaultValue={copy.list?.[0]?.label || ''}
146
149
  onValueChange={value => {
147
- setActiveTab(value)
148
150
  gaTrack({
149
151
  event: 'ga4Event',
150
152
  event_name: 'lp_navigation',
@@ -162,32 +164,38 @@ export const CreditsRedeemList = ({ copy, id }: { copy: CreditsRedeemListCopy; i
162
164
  </TabsTrigger>
163
165
  ))}
164
166
  </TabsList>
167
+ {(copy.list || []).map(categoryItem => (
168
+ <TabsContent key={categoryItem.label} value={categoryItem.label}>
169
+ {/* PC端 */}
170
+ {!!alpcList?.length && !listLoading && (
171
+ <div
172
+ className={classNames(
173
+ 'relative grid grid-cols-4 gap-[16px] md:grid-cols-2 md:gap-[12px] md-xl:grid-cols-3'
174
+ )}
175
+ >
176
+ {(listByTab.get(categoryItem.label) || [])?.map(item => (
177
+ <RedeemableItem
178
+ key={item?.alpc?.id}
179
+ copy={copy}
180
+ item={item}
181
+ onRedeem={(item: RedeemableItemType) => {
182
+ if (!isLogin) {
183
+ openSignUpPopup()
184
+ } else if (!profile?.activated && !authCodeActivate.isActivateSuccess) {
185
+ authCodeActivate.setAutoSendEmail(true)
186
+ authCodeActivate.open()
187
+ } else {
188
+ setPopRedeemData(item)
189
+ }
190
+ }}
191
+ onRulesOpen={setRules}
192
+ />
193
+ ))}
194
+ </div>
195
+ )}
196
+ </TabsContent>
197
+ ))}
165
198
  </Tabs>
166
- {/* PC端 */}
167
- {!!alpcList?.length && !listLoading && (
168
- <div
169
- className={classNames('relative grid grid-cols-4 gap-[16px] md:grid-cols-2 md:gap-[12px] md-xl:grid-cols-3')}
170
- >
171
- {list?.map(item => (
172
- <RedeemableItem
173
- key={item?.alpc?.id}
174
- copy={copy}
175
- item={item}
176
- onRedeem={(item: RedeemableItemType) => {
177
- if (!isLogin) {
178
- openSignUpPopup()
179
- } else if (!profile?.activated && !authCodeActivate.isActivateSuccess) {
180
- authCodeActivate.setAutoSendEmail(true)
181
- authCodeActivate.open()
182
- } else {
183
- setPopRedeemData(item)
184
- }
185
- }}
186
- onRulesOpen={setRules}
187
- />
188
- ))}
189
- </div>
190
- )}
191
199
 
192
200
  {(popRedeemData?.config?.type === ConsumeType.Coupon ||
193
201
  popRedeemData?.config?.type === ConsumeType.ShippingCoupon) &&
@@ -5,7 +5,7 @@ import useActivities from '../context/hooks/useActivities'
5
5
  import type { Task } from '../context/response'
6
6
  import { CreditsModalContainer, type ModalContainerProps } from './ModalContainer'
7
7
  import { classNames as cn, useHeadlessContext } from '@anker-in/lib'
8
- import { Tabs, TabsList, TabsTrigger } from '@anker-in/headless-ui'
8
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@anker-in/headless-ui'
9
9
  import LoadingDots from './LoadingDots'
10
10
  import { Picture, Text } from '@anker-in/headless-ui'
11
11
  import { ROUNDED_BRANDS } from '../../../constants'
@@ -55,7 +55,6 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
55
55
  }
56
56
  }, [props.isOpen, page])
57
57
 
58
- const [activeTab, setActiveTab] = useState(CreditType.All)
59
58
  const tabs = useMemo(() => {
60
59
  const tabs = [
61
60
  {
@@ -82,7 +81,7 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
82
81
 
83
82
  const { activities = [], canNext, isLoading } = useActivities(myActivitiesOptions)
84
83
 
85
- const list = useMemo(() => {
84
+ const allList = useMemo(() => {
86
85
  const taskNameMap = {
87
86
  [TaskSubType.Activation]: data.activationTask,
88
87
  [TaskSubType.UpdateName]: data.updateNameTask,
@@ -114,7 +113,7 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
114
113
  }
115
114
  }
116
115
 
117
- const all = activities.map(item => ({
116
+ return activities.map(item => ({
118
117
  title: getTitle(item),
119
118
  date: new Date(item.create_time * 1000),
120
119
  value: item.credit,
@@ -123,27 +122,6 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
123
122
  credit_type: item.credit_type,
124
123
  status: item.status,
125
124
  }))
126
-
127
- let filtered = all || []
128
- if (activeTab === CreditType.Earned) {
129
- filtered = filtered?.filter(item => item.type === 1)
130
- } else if (activeTab === CreditType.Deducted) {
131
- filtered = filtered?.filter(item => item.type === 2 && item.status !== 3)
132
- } else if (activeTab === CreditType.Expired) {
133
- filtered = filtered?.filter(item => item.type === 2 && item.status === 3)
134
- }
135
-
136
- return filtered.map(item => {
137
- let { value, ...otherItem } = item
138
- if (item.type === CreditType.Deducted && value > 0) {
139
- value = -value
140
- }
141
-
142
- return {
143
- value,
144
- ...otherItem,
145
- }
146
- })
147
125
  }, [
148
126
  data.activationTask,
149
127
  data.updateNameTask,
@@ -157,10 +135,36 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
157
135
  data.uploadReceiptTask,
158
136
  data.shoppingTask,
159
137
  activities,
160
- activeTab,
161
138
  taskIdToTypeMapping,
162
139
  ])
163
140
 
141
+ const normalizeValue = useCallback((item: (typeof allList)[number]) => {
142
+ let { value, ...otherItem } = item
143
+ if (item.type === CreditType.Deducted && value > 0) {
144
+ value = -value
145
+ }
146
+ return { value, ...otherItem }
147
+ }, [])
148
+
149
+ // 按 tab 类型预计算列表,配合 TabsContent 声明式渲染,避免手动 activeTab 数值比较
150
+ const listByTab = useMemo(() => {
151
+ const map = new Map<CreditType, ReturnType<typeof normalizeValue>[]>()
152
+ map.set(CreditType.All, allList.map(normalizeValue))
153
+ map.set(
154
+ CreditType.Earned,
155
+ allList.filter(item => item.type === 1).map(normalizeValue)
156
+ )
157
+ map.set(
158
+ CreditType.Deducted,
159
+ allList.filter(item => item.type === 2 && item.status !== 3).map(normalizeValue)
160
+ )
161
+ map.set(
162
+ CreditType.Expired,
163
+ allList.filter(item => item.type === 2 && item.status === 3).map(normalizeValue)
164
+ )
165
+ return map
166
+ }, [allList, normalizeValue])
167
+
164
168
  const handleScrollEnd = useCallback(() => {
165
169
  if (!canNext) {
166
170
  return
@@ -168,36 +172,8 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
168
172
  setPage(page + 1)
169
173
  }, [page, canNext])
170
174
 
171
- return (
172
- <CreditsModalContainer
173
- title={data.title}
174
- className={cn('h-[800px] w-[640px]')}
175
- useAnimation
176
- animationClassName="md:translate-y-[100vh]"
177
- scrollClassName="flex flex-col"
178
- {...props}
179
- onScrollEnd={handleScrollEnd}
180
- >
181
- <div className="w-full sticky top-0 bg-white z-10 pb-[24px] overflow-x-auto">
182
- <Tabs
183
- align="left"
184
- shape={rounded ? 'rounded' : 'square'}
185
- value={activeTab.toString()}
186
- onValueChange={value => {
187
- setActiveTab(Number(value))
188
- }}
189
- className=""
190
- >
191
- <TabsList>
192
- {tabs.map(tab => (
193
- <TabsTrigger key={tab.type} value={tab.type.toString()}>
194
- {tab.title}
195
- </TabsTrigger>
196
- ))}
197
- </TabsList>
198
- </Tabs>
199
- </div>
200
-
175
+ const renderList = (list: ReturnType<typeof normalizeValue>[]) => (
176
+ <>
201
177
  <div className="flex-1 overflow-auto overscroll-contain">
202
178
  <div className="grid gap-[18px] md:gap-[12px]">
203
179
  {isLoading && (
@@ -259,6 +235,36 @@ function ActivitiesModal({ data, ...props }: ActivitiesModalProps) {
259
235
  </div>
260
236
  {/* 积分到账时间提醒 */}
261
237
  {list.length > 0 && <div className="mt-[12px] pb-[24px] text-[14px] font-semibold text-[#999]">{data.tips}</div>}
238
+ </>
239
+ )
240
+
241
+ return (
242
+ <CreditsModalContainer
243
+ title={data.title}
244
+ className={cn('h-[800px] w-[640px]')}
245
+ useAnimation
246
+ animationClassName="md:translate-y-[100vh]"
247
+ scrollClassName="flex flex-col"
248
+ {...props}
249
+ onScrollEnd={handleScrollEnd}
250
+ >
251
+ <Tabs align="left" shape={rounded ? 'rounded' : 'square'} defaultValue={tabs[0]?.type?.toString()} className="flex-1 flex flex-col">
252
+ <div className="w-full sticky top-0 bg-white z-10 pb-[24px] overflow-x-auto">
253
+ <TabsList>
254
+ {tabs.map(tab => (
255
+ <TabsTrigger key={tab.type} value={tab.type.toString()}>
256
+ {tab.title}
257
+ </TabsTrigger>
258
+ ))}
259
+ </TabsList>
260
+ </div>
261
+
262
+ {tabs.map(tab => (
263
+ <TabsContent key={tab.type} value={tab.type.toString()} className="flex-1 flex flex-col">
264
+ {renderList(listByTab.get(tab.type) || [])}
265
+ </TabsContent>
266
+ ))}
267
+ </Tabs>
262
268
  </CreditsModalContainer>
263
269
  )
264
270
  }
@@ -1,4 +1,4 @@
1
- import { Tabs, TabsList, TabsTrigger, Picture } from '@anker-in/headless-ui'
1
+ import { Tabs, TabsContent, TabsList, TabsTrigger, Picture } from '@anker-in/headless-ui'
2
2
  import classNames from 'classnames'
3
3
  import clipboard from 'copy-to-clipboard'
4
4
  import dayjs from 'dayjs'
@@ -245,35 +245,10 @@ function RewardsModal({ data, ...props }: RewardsModalProps) {
245
245
  }, [])
246
246
 
247
247
 
248
- return (
249
- <CreditsModalContainer
250
- title={data.title}
251
- className={classNames('h-[800px] w-[640px]')}
252
- useAnimation
253
- animationClassName="md:translate-y-[100vh]"
254
- scrollClassName="md:mb-0 flex flex-col"
255
- {...props}
256
- onScrollEnd={handleScrollEnd}
257
- >
258
- <div className="shrink-0">
259
- <Tabs
260
- align="left"
261
- shape={rounded ? 'rounded' : 'square'}
262
- value={activeTab.toString()}
263
- onValueChange={value => {
264
- setActiveTab(Number(value))
265
- }}
266
- >
267
- <TabsList>
268
- {tabs.map(tab => (
269
- <TabsTrigger key={tab.type} value={tab.type.toString()}>
270
- {tab.title}
271
- </TabsTrigger>
272
- ))}
273
- </TabsList>
274
- </Tabs>
275
- </div>
276
-
248
+ // 每个 tab 对应一次独立的接口请求(consumeType 不同),当前只有激活 tab 的数据存在于客户端。
249
+ // TabsContent 只挂载当前激活的一个,各 tab 复用同一份 list/isLoading 状态是安全的。
250
+ const renderContent = () => (
251
+ <>
277
252
  {isLoading && (
278
253
  <div className="flex flex-1 flex-col items-center justify-center">
279
254
  <LoadingDots />
@@ -365,6 +340,44 @@ function RewardsModal({ data, ...props }: RewardsModalProps) {
365
340
  )}
366
341
  </div>
367
342
  )}
343
+ </>
344
+ )
345
+
346
+ return (
347
+ <CreditsModalContainer
348
+ title={data.title}
349
+ className={classNames('h-[800px] w-[640px]')}
350
+ useAnimation
351
+ animationClassName="md:translate-y-[100vh]"
352
+ scrollClassName="md:mb-0 flex flex-col"
353
+ {...props}
354
+ onScrollEnd={handleScrollEnd}
355
+ >
356
+ <Tabs
357
+ align="left"
358
+ shape={rounded ? 'rounded' : 'square'}
359
+ value={activeTab.toString()}
360
+ onValueChange={value => {
361
+ setActiveTab(Number(value))
362
+ }}
363
+ className="flex flex-1 flex-col"
364
+ >
365
+ <div className="shrink-0">
366
+ <TabsList>
367
+ {tabs.map(tab => (
368
+ <TabsTrigger key={tab.type} value={tab.type.toString()}>
369
+ {tab.title}
370
+ </TabsTrigger>
371
+ ))}
372
+ </TabsList>
373
+ </div>
374
+
375
+ {tabs.map(tab => (
376
+ <TabsContent key={tab.type} value={tab.type.toString()} className="flex flex-1 flex-col">
377
+ {renderContent()}
378
+ </TabsContent>
379
+ ))}
380
+ </Tabs>
368
381
  </CreditsModalContainer>
369
382
  )
370
383
  }