payment-kit 1.18.18 → 1.18.19
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/api/src/libs/subscription.ts +116 -0
- package/api/src/routes/checkout-sessions.ts +28 -1
- package/api/src/routes/customers.ts +5 -1
- package/api/src/store/migrations/20250318-donate-invoice.ts +45 -0
- package/api/tests/libs/subscription.spec.ts +311 -0
- package/blocklet.yml +1 -1
- package/package.json +8 -8
- package/src/components/currency.tsx +11 -4
- package/src/components/customer/link.tsx +54 -14
- package/src/components/customer/overdraft-protection.tsx +36 -2
- package/src/components/info-card.tsx +55 -7
- package/src/components/info-row-group.tsx +122 -0
- package/src/components/info-row.tsx +14 -1
- package/src/components/payouts/portal/list.tsx +7 -2
- package/src/components/subscription/items/index.tsx +1 -1
- package/src/components/subscription/metrics.tsx +14 -6
- package/src/contexts/info-row.tsx +4 -0
- package/src/locales/en.tsx +1 -0
- package/src/locales/zh.tsx +1 -0
- package/src/pages/admin/billing/invoices/detail.tsx +54 -76
- package/src/pages/admin/billing/subscriptions/detail.tsx +34 -71
- package/src/pages/admin/customers/customers/detail.tsx +41 -64
- package/src/pages/admin/payments/intents/detail.tsx +28 -42
- package/src/pages/admin/payments/payouts/detail.tsx +27 -36
- package/src/pages/admin/payments/refunds/detail.tsx +27 -41
- package/src/pages/admin/products/links/detail.tsx +30 -55
- package/src/pages/admin/products/prices/detail.tsx +43 -50
- package/src/pages/admin/products/pricing-tables/detail.tsx +23 -25
- package/src/pages/admin/products/products/detail.tsx +52 -81
- package/src/pages/customer/index.tsx +183 -108
- package/src/pages/customer/invoice/detail.tsx +49 -50
- package/src/pages/customer/payout/detail.tsx +16 -22
- package/src/pages/customer/recharge/account.tsx +92 -34
- package/src/pages/customer/recharge/subscription.tsx +6 -0
- package/src/pages/customer/subscription/detail.tsx +176 -94
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
Button,
|
|
12
12
|
CircularProgress,
|
|
13
13
|
Divider,
|
|
14
|
-
Grid,
|
|
15
14
|
Stack,
|
|
16
15
|
Tooltip,
|
|
17
16
|
Typography,
|
|
@@ -25,6 +24,7 @@ import Copyable from '../../../../components/copyable';
|
|
|
25
24
|
import EventList from '../../../../components/event/list';
|
|
26
25
|
import InfoMetric from '../../../../components/info-metric';
|
|
27
26
|
import InfoRow from '../../../../components/info-row';
|
|
27
|
+
import InfoRowGroup from '../../../../components/info-row-group';
|
|
28
28
|
import MetadataEditor from '../../../../components/metadata/editor';
|
|
29
29
|
import MetadataList from '../../../../components/metadata/list';
|
|
30
30
|
import ProductActions from '../../../../components/product/actions';
|
|
@@ -39,9 +39,6 @@ const getProduct = (id: string): Promise<TProductExpanded> => {
|
|
|
39
39
|
return api.get(`/api/products/${id}`).then((res) => res.data);
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const InfoDirection = 'column';
|
|
43
|
-
const InfoAlignItems = 'flex-start';
|
|
44
|
-
|
|
45
42
|
export default function ProductDetail(props: { id: string }) {
|
|
46
43
|
const { t, locale } = useLocaleContext();
|
|
47
44
|
const { isMobile } = useMobile();
|
|
@@ -212,6 +209,7 @@ export default function ProductDetail(props: { id: string }) {
|
|
|
212
209
|
},
|
|
213
210
|
}}>
|
|
214
211
|
<InfoMetric label={t('common.id')} value={<Copyable text={props.id} style={{ marginLeft: 4 }} />} divider />
|
|
212
|
+
<InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
|
|
215
213
|
</Stack>
|
|
216
214
|
</Box>
|
|
217
215
|
<Divider />
|
|
@@ -246,7 +244,7 @@ export default function ProductDetail(props: { id: string }) {
|
|
|
246
244
|
},
|
|
247
245
|
}}>
|
|
248
246
|
<Box flex={1} className="payment-link-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
249
|
-
<Box className="section">
|
|
247
|
+
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
250
248
|
<SectionHeader title={t('admin.details')}>
|
|
251
249
|
<Button
|
|
252
250
|
variant="text"
|
|
@@ -257,83 +255,56 @@ export default function ProductDetail(props: { id: string }) {
|
|
|
257
255
|
{t('common.edit')}
|
|
258
256
|
</Button>
|
|
259
257
|
</SectionHeader>
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
258
|
+
<InfoRowGroup
|
|
259
|
+
sx={{
|
|
260
|
+
display: 'grid',
|
|
261
|
+
gridTemplateColumns: {
|
|
262
|
+
xs: 'repeat(1, 1fr)',
|
|
263
|
+
xl: 'repeat(2, 1fr)',
|
|
264
|
+
},
|
|
265
|
+
'@container (min-width: 1000px)': {
|
|
266
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
267
|
+
},
|
|
268
|
+
'.info-row-wrapper': {
|
|
269
|
+
gap: 1,
|
|
270
|
+
flexDirection: {
|
|
271
|
+
xs: 'column',
|
|
272
|
+
xl: 'row',
|
|
271
273
|
},
|
|
272
|
-
|
|
273
|
-
xs:
|
|
274
|
-
|
|
274
|
+
alignItems: {
|
|
275
|
+
xs: 'flex-start',
|
|
276
|
+
xl: 'center',
|
|
275
277
|
},
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
/>
|
|
307
|
-
<InfoRow
|
|
308
|
-
label={t('common.createdAt')}
|
|
309
|
-
value={formatTime(data.created_at)}
|
|
310
|
-
direction={InfoDirection}
|
|
311
|
-
alignItems={InfoAlignItems}
|
|
312
|
-
/>
|
|
313
|
-
<InfoRow
|
|
314
|
-
label={t('common.updatedAt')}
|
|
315
|
-
value={formatTime(data.updated_at)}
|
|
316
|
-
direction={InfoDirection}
|
|
317
|
-
alignItems={InfoAlignItems}
|
|
318
|
-
/>
|
|
319
|
-
<InfoRow
|
|
320
|
-
label={t('admin.product.image.label')}
|
|
321
|
-
value={
|
|
322
|
-
data.images.length ? (
|
|
323
|
-
<Avatar
|
|
324
|
-
src={data.images[0]}
|
|
325
|
-
alt={data.name}
|
|
326
|
-
variant="square"
|
|
327
|
-
sx={{ width: '160px', height: '160px' }}
|
|
328
|
-
/>
|
|
329
|
-
) : (
|
|
330
|
-
t('empty.image')
|
|
331
|
-
)
|
|
332
|
-
}
|
|
333
|
-
direction={InfoDirection}
|
|
334
|
-
alignItems={InfoAlignItems}
|
|
335
|
-
/>
|
|
336
|
-
</Grid>
|
|
278
|
+
'@container (min-width: 1000px)': {
|
|
279
|
+
flexDirection: 'row',
|
|
280
|
+
alignItems: 'center',
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
}}>
|
|
284
|
+
<InfoRow label={t('admin.product.name.label')} value={data.name} />
|
|
285
|
+
|
|
286
|
+
<InfoRow label={t('admin.product.description.label')} value={data.description} />
|
|
287
|
+
<InfoRow label={t('admin.product.statement_descriptor.label')} value={data.statement_descriptor} />
|
|
288
|
+
<InfoRow label={t('admin.product.unit_label.label')} value={data.unit_label} />
|
|
289
|
+
<InfoRow label={t('admin.product.features.label')} value={data.features.map((x) => x.name).join(',')} />
|
|
290
|
+
<InfoRow label={t('common.updatedAt')} value={formatTime(data.updated_at)} />
|
|
291
|
+
<InfoRow
|
|
292
|
+
label={t('admin.product.image.label')}
|
|
293
|
+
value={
|
|
294
|
+
data.images.length ? (
|
|
295
|
+
<Avatar
|
|
296
|
+
src={data.images[0]}
|
|
297
|
+
alt={data.name}
|
|
298
|
+
variant="square"
|
|
299
|
+
sx={{ width: '160px', height: '160px' }}
|
|
300
|
+
/>
|
|
301
|
+
) : (
|
|
302
|
+
<Typography variant="body2" color="text.lighter">
|
|
303
|
+
{t('empty.image')}
|
|
304
|
+
</Typography>
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
337
308
|
{state.editing.product && (
|
|
338
309
|
<EditProduct
|
|
339
310
|
product={data}
|
|
@@ -342,7 +313,7 @@ export default function ProductDetail(props: { id: string }) {
|
|
|
342
313
|
onCancel={() => setState((prev) => ({ editing: { ...prev.editing, product: false } }))}
|
|
343
314
|
/>
|
|
344
315
|
)}
|
|
345
|
-
</
|
|
316
|
+
</InfoRowGroup>
|
|
346
317
|
</Box>
|
|
347
318
|
<Divider />
|
|
348
319
|
<Box className="section">
|
|
@@ -78,11 +78,7 @@ const CurrencyCard = memo(
|
|
|
78
78
|
const method = settings?.paymentMethods?.find((m) => m.id === currency.payment_method_id);
|
|
79
79
|
const { t } = useLocaleContext();
|
|
80
80
|
const safeData = data || {};
|
|
81
|
-
const value = formatBNStr(safeData[currency?.id], currency?.decimal, 6,
|
|
82
|
-
const hideTypes = ['stake', 'refund', 'due'];
|
|
83
|
-
if (hideTypes.includes(type) && (!safeData[currency?.id] || safeData[currency?.id] === '0')) {
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
81
|
+
const value = formatBNStr(safeData[currency?.id], currency?.decimal, 6, true);
|
|
86
82
|
|
|
87
83
|
const handleCardClick = () => {
|
|
88
84
|
if (type === 'balance' && currency?.id) {
|
|
@@ -95,7 +91,11 @@ const CurrencyCard = memo(
|
|
|
95
91
|
sx={{
|
|
96
92
|
transition: 'all 0.2s ease-in-out',
|
|
97
93
|
position: 'relative',
|
|
98
|
-
|
|
94
|
+
height: '100%',
|
|
95
|
+
display: 'flex',
|
|
96
|
+
flexDirection: 'column',
|
|
97
|
+
justifyContent: 'space-between',
|
|
98
|
+
containerType: 'inline-size',
|
|
99
99
|
}}>
|
|
100
100
|
<Typography
|
|
101
101
|
variant="body1"
|
|
@@ -126,8 +126,20 @@ const CurrencyCard = memo(
|
|
|
126
126
|
'&:hover': {
|
|
127
127
|
color: 'text.link',
|
|
128
128
|
},
|
|
129
|
+
'.recharge-title': {
|
|
130
|
+
'@container (max-width: 230px)': {
|
|
131
|
+
display: 'none',
|
|
132
|
+
},
|
|
133
|
+
'@container (min-width: 231px)': {
|
|
134
|
+
display: 'inline-block',
|
|
135
|
+
},
|
|
136
|
+
},
|
|
129
137
|
}}>
|
|
130
138
|
<AddOutlined fontSize="small" />
|
|
139
|
+
|
|
140
|
+
<Typography variant="body2" className="recharge-title">
|
|
141
|
+
{t('customer.recharge.title')}
|
|
142
|
+
</Typography>
|
|
131
143
|
</Box>
|
|
132
144
|
</Tooltip>
|
|
133
145
|
)}
|
|
@@ -156,11 +168,73 @@ function SummaryCardSkeleton() {
|
|
|
156
168
|
);
|
|
157
169
|
}
|
|
158
170
|
|
|
171
|
+
function isSummaryEmpty(summary: any, id: string) {
|
|
172
|
+
const summaryKeys = ['balance', 'paid', 'stake', 'refund', 'due'];
|
|
173
|
+
return summaryKeys.every((key) => !summary?.[key]?.[id] || summary?.[key]?.[id] === '0');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 定义卡片类型
|
|
177
|
+
type CardType = 'balance' | 'spent' | 'stake' | 'refund' | 'due';
|
|
178
|
+
|
|
179
|
+
// 定义一个统一的卡片配置
|
|
180
|
+
const CARD_CONFIG: Record<CardType, { key: keyof typeof summaryKeyMap; alwaysShow?: boolean }> = {
|
|
181
|
+
balance: { key: 'token', alwaysShow: true },
|
|
182
|
+
spent: { key: 'paid', alwaysShow: true },
|
|
183
|
+
stake: { key: 'stake' },
|
|
184
|
+
refund: { key: 'refund' },
|
|
185
|
+
due: { key: 'due' },
|
|
186
|
+
} as const;
|
|
187
|
+
|
|
188
|
+
// 映射 summary 中的键名
|
|
189
|
+
const summaryKeyMap = {
|
|
190
|
+
token: 'token',
|
|
191
|
+
paid: 'paid',
|
|
192
|
+
stake: 'stake',
|
|
193
|
+
refund: 'refund',
|
|
194
|
+
due: 'due',
|
|
195
|
+
} as const;
|
|
196
|
+
|
|
197
|
+
// 优化后的计算函数
|
|
198
|
+
const getCardVisibility = (currency: any, data: any) => {
|
|
199
|
+
return Object.entries(CARD_CONFIG).reduce(
|
|
200
|
+
(acc, [type, config]) => {
|
|
201
|
+
const summaryKey = config.key;
|
|
202
|
+
const isVisible =
|
|
203
|
+
config.alwaysShow ||
|
|
204
|
+
(data?.summary?.[summaryKey]?.[currency.id] && data?.summary?.[summaryKey]?.[currency.id] !== '0');
|
|
205
|
+
|
|
206
|
+
if (isVisible) {
|
|
207
|
+
acc.visibleCards.push(type as CardType);
|
|
208
|
+
}
|
|
209
|
+
return acc;
|
|
210
|
+
},
|
|
211
|
+
{ visibleCards: [] as CardType[] }
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// 计算最大卡片数量
|
|
216
|
+
const calculateMaxCardCount = (currencies: any[], data: any) => {
|
|
217
|
+
return currencies.reduce((maxCount, currency) => {
|
|
218
|
+
const { visibleCards } = getCardVisibility(currency, data);
|
|
219
|
+
return Math.max(maxCount, visibleCards.length);
|
|
220
|
+
}, 0);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const getCardIcon = (type: CardType) => {
|
|
224
|
+
const iconMap = {
|
|
225
|
+
balance: <AccountBalanceWalletOutlined color="success" fontSize="small" />,
|
|
226
|
+
spent: <CreditCardOutlined color="warning" fontSize="small" />,
|
|
227
|
+
stake: <AccountBalanceOutlined fontSize="small" color="info" />,
|
|
228
|
+
refund: <AssignmentReturnOutlined fontSize="small" sx={{ color: 'var(--tags-tag-purple-icon, #7c3aed)' }} />,
|
|
229
|
+
due: <InfoOutlined fontSize="small" color="error" />,
|
|
230
|
+
};
|
|
231
|
+
return iconMap[type];
|
|
232
|
+
};
|
|
233
|
+
|
|
159
234
|
export default function CustomerHome() {
|
|
160
235
|
const { t } = useLocaleContext();
|
|
161
236
|
const { events } = useSessionContext();
|
|
162
237
|
const { settings } = usePaymentContext();
|
|
163
|
-
const [currency, setCurrency] = useState(settings?.baseCurrency);
|
|
164
238
|
const [subscriptionLoading, setSubscriptionLoading] = useState(false);
|
|
165
239
|
const currencies = flatten(
|
|
166
240
|
settings.paymentMethods?.map((method) =>
|
|
@@ -250,10 +324,6 @@ export default function CustomerHome() {
|
|
|
250
324
|
setSubscriptionLoading(false);
|
|
251
325
|
}, 300);
|
|
252
326
|
};
|
|
253
|
-
const handleCurrencyChange = (e: SelectChangeEvent) => {
|
|
254
|
-
const newCurrency = currencies.find((c) => c.id === e.target.value) || settings?.baseCurrency;
|
|
255
|
-
setCurrency(newCurrency);
|
|
256
|
-
};
|
|
257
327
|
|
|
258
328
|
const SubscriptionCard =
|
|
259
329
|
loadingCard || !hasSubscriptions ? null : (
|
|
@@ -308,6 +378,14 @@ export default function CustomerHome() {
|
|
|
308
378
|
</Box>
|
|
309
379
|
);
|
|
310
380
|
|
|
381
|
+
const maxCardCount = calculateMaxCardCount(currencies, data);
|
|
382
|
+
|
|
383
|
+
const responsiveColumns = {
|
|
384
|
+
xs: Math.min(2, maxCardCount),
|
|
385
|
+
sm: Math.min(3, maxCardCount),
|
|
386
|
+
md: maxCardCount,
|
|
387
|
+
};
|
|
388
|
+
|
|
311
389
|
const SummaryCard = loadingCard ? (
|
|
312
390
|
<SummaryCardSkeleton />
|
|
313
391
|
) : (
|
|
@@ -316,104 +394,101 @@ export default function CustomerHome() {
|
|
|
316
394
|
<>
|
|
317
395
|
<Box className="section-header">
|
|
318
396
|
<Typography variant="h3">{t('admin.customer.summary.stats')}</Typography>
|
|
319
|
-
<FormControl
|
|
320
|
-
sx={{
|
|
321
|
-
'.MuiInputBase-root': {
|
|
322
|
-
background: 'none',
|
|
323
|
-
border: 'none',
|
|
324
|
-
},
|
|
325
|
-
'.MuiOutlinedInput-notchedOutline': {
|
|
326
|
-
border: 'none',
|
|
327
|
-
},
|
|
328
|
-
}}>
|
|
329
|
-
<Select
|
|
330
|
-
value={currency?.id}
|
|
331
|
-
onChange={handleCurrencyChange}
|
|
332
|
-
displayEmpty
|
|
333
|
-
IconComponent={ExpandMore}
|
|
334
|
-
inputProps={{ 'aria-label': 'Without label' }}>
|
|
335
|
-
{currencies.map((c) => (
|
|
336
|
-
<MenuItem key={c.id} value={c.id}>
|
|
337
|
-
<Box alignItems="center" display="flex" gap={1}>
|
|
338
|
-
<Avatar src={c?.logo} alt={c?.symbol} sx={{ width: 18, height: 18 }} />
|
|
339
|
-
<Typography
|
|
340
|
-
variant="h5"
|
|
341
|
-
component="div"
|
|
342
|
-
sx={{ fontSize: '16px', color: 'text.primary', fontWeight: '500' }}>
|
|
343
|
-
{c?.symbol}
|
|
344
|
-
</Typography>
|
|
345
|
-
<Typography sx={{ fontSize: 12 }} color="text.lighter">
|
|
346
|
-
{c?.methodName}
|
|
347
|
-
</Typography>
|
|
348
|
-
</Box>
|
|
349
|
-
</MenuItem>
|
|
350
|
-
))}
|
|
351
|
-
</Select>
|
|
352
|
-
</FormControl>
|
|
353
397
|
</Box>
|
|
354
|
-
<Stack
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
398
|
+
<Stack gap={2} mt={2}>
|
|
399
|
+
{currencies.map((c) => {
|
|
400
|
+
return isSummaryEmpty(data?.summary, c.id) && c.id !== settings.baseCurrency.id ? null : (
|
|
401
|
+
<Stack
|
|
402
|
+
key={c.id}
|
|
403
|
+
sx={{
|
|
404
|
+
pb: 2,
|
|
405
|
+
borderBottom: '1px solid var(--stroke-border-base, #EFF1F5)',
|
|
406
|
+
'&:last-child': { borderBottom: 'none', pb: 0 },
|
|
407
|
+
}}>
|
|
408
|
+
<Box alignItems="center" display="flex" gap={1}>
|
|
409
|
+
<Avatar src={c?.logo} alt={c?.symbol} sx={{ width: 18, height: 18 }} />
|
|
410
|
+
<Typography
|
|
411
|
+
variant="h5"
|
|
412
|
+
component="div"
|
|
413
|
+
sx={{ fontSize: '16px', color: 'text.primary', fontWeight: '500' }}>
|
|
414
|
+
{c?.symbol}
|
|
415
|
+
</Typography>
|
|
416
|
+
<Typography sx={{ fontSize: 12 }} color="text.lighter">
|
|
417
|
+
{c?.methodName}
|
|
418
|
+
</Typography>
|
|
419
|
+
</Box>
|
|
420
|
+
<Stack
|
|
421
|
+
className="section-body"
|
|
422
|
+
flexDirection="row"
|
|
423
|
+
sx={{
|
|
424
|
+
gap: 3,
|
|
425
|
+
mt: 1.5,
|
|
426
|
+
flexWrap: 'wrap',
|
|
427
|
+
position: 'relative',
|
|
428
|
+
display: 'grid',
|
|
429
|
+
gridTemplateColumns: {
|
|
430
|
+
xs: `repeat(${responsiveColumns.xs}, 1fr)`,
|
|
431
|
+
sm: `repeat(${responsiveColumns.sm}, 1fr)`,
|
|
432
|
+
md: `repeat(${responsiveColumns.md}, 1fr)`,
|
|
433
|
+
},
|
|
434
|
+
'&::after': {
|
|
435
|
+
content: '""',
|
|
436
|
+
position: 'absolute',
|
|
437
|
+
right: 0,
|
|
438
|
+
top: 0,
|
|
439
|
+
bottom: 0,
|
|
440
|
+
width: '1px',
|
|
441
|
+
backgroundColor: 'background.paper',
|
|
442
|
+
zIndex: 1,
|
|
443
|
+
},
|
|
444
|
+
}}>
|
|
445
|
+
{/* 使用配置渲染卡片 */}
|
|
446
|
+
{Object.entries(CARD_CONFIG).map(([type, config]) => {
|
|
447
|
+
const summaryKey = config.key;
|
|
448
|
+
const isVisible =
|
|
449
|
+
config.alwaysShow ||
|
|
450
|
+
(data?.summary?.[summaryKey]?.[c.id] && data?.summary?.[summaryKey]?.[c.id] !== '0');
|
|
451
|
+
|
|
452
|
+
if (!isVisible) return null;
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<Box
|
|
456
|
+
key={`card-${c.id}-${type}`}
|
|
457
|
+
className="card-item"
|
|
458
|
+
sx={{
|
|
459
|
+
position: 'relative',
|
|
460
|
+
'&::after': {
|
|
461
|
+
content: '""',
|
|
462
|
+
position: 'absolute',
|
|
463
|
+
right: 0,
|
|
464
|
+
top: 0,
|
|
465
|
+
bottom: 0,
|
|
466
|
+
width: '1px',
|
|
467
|
+
backgroundColor: 'var(--stroke-border-base, #EFF1F5)',
|
|
468
|
+
zIndex: 1,
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
'&:has(+ .placeholder)::after': {
|
|
472
|
+
display: 'none',
|
|
473
|
+
},
|
|
474
|
+
'&:last-child::after': {
|
|
475
|
+
display: 'none',
|
|
476
|
+
},
|
|
477
|
+
}}>
|
|
478
|
+
<CurrencyCard
|
|
479
|
+
label={t(`admin.customer.summary.${type}`)}
|
|
480
|
+
data={data?.summary?.[summaryKey] || emptyObject}
|
|
481
|
+
currency={c}
|
|
482
|
+
type={type as CardType}
|
|
483
|
+
icon={getCardIcon(type as CardType)}
|
|
484
|
+
/>
|
|
485
|
+
</Box>
|
|
486
|
+
);
|
|
487
|
+
})}
|
|
488
|
+
</Stack>
|
|
489
|
+
</Stack>
|
|
490
|
+
);
|
|
491
|
+
})}
|
|
417
492
|
</Stack>
|
|
418
493
|
</>
|
|
419
494
|
)}
|