@blocklet/payment-react 1.19.0 → 1.19.2

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 (75) hide show
  1. package/es/components/blockchain/tx.d.ts +1 -1
  2. package/es/components/blockchain/tx.js +9 -11
  3. package/es/components/country-select.d.ts +1 -1
  4. package/es/components/date-range-picker.d.ts +13 -0
  5. package/es/components/date-range-picker.js +279 -0
  6. package/es/components/input.d.ts +5 -2
  7. package/es/components/input.js +6 -2
  8. package/es/components/label.d.ts +7 -0
  9. package/es/components/label.js +50 -0
  10. package/es/components/loading-button.d.ts +1 -1
  11. package/es/history/credit/grants-list.d.ts +14 -0
  12. package/es/history/credit/grants-list.js +215 -0
  13. package/es/history/credit/transactions-list.d.ts +13 -0
  14. package/es/history/credit/transactions-list.js +254 -0
  15. package/es/history/invoice/list.js +21 -1
  16. package/es/index.d.ts +5 -1
  17. package/es/index.js +10 -1
  18. package/es/libs/util.d.ts +2 -0
  19. package/es/libs/util.js +12 -0
  20. package/es/locales/en.js +20 -2
  21. package/es/locales/zh.js +20 -2
  22. package/es/payment/form/address.js +2 -1
  23. package/es/payment/form/index.js +46 -7
  24. package/es/payment/index.js +18 -3
  25. package/es/payment/product-item.d.ts +8 -1
  26. package/es/payment/product-item.js +137 -5
  27. package/es/payment/summary.d.ts +3 -1
  28. package/es/payment/summary.js +9 -0
  29. package/lib/components/blockchain/tx.d.ts +1 -1
  30. package/lib/components/blockchain/tx.js +9 -8
  31. package/lib/components/country-select.d.ts +1 -1
  32. package/lib/components/date-range-picker.d.ts +13 -0
  33. package/lib/components/date-range-picker.js +329 -0
  34. package/lib/components/input.d.ts +5 -2
  35. package/lib/components/input.js +8 -4
  36. package/lib/components/label.d.ts +7 -0
  37. package/lib/components/label.js +62 -0
  38. package/lib/components/loading-button.d.ts +1 -1
  39. package/lib/history/credit/grants-list.d.ts +14 -0
  40. package/lib/history/credit/grants-list.js +277 -0
  41. package/lib/history/credit/transactions-list.d.ts +13 -0
  42. package/lib/history/credit/transactions-list.js +300 -0
  43. package/lib/history/invoice/list.js +24 -0
  44. package/lib/index.d.ts +5 -1
  45. package/lib/index.js +39 -0
  46. package/lib/libs/util.d.ts +2 -0
  47. package/lib/libs/util.js +14 -0
  48. package/lib/locales/en.js +20 -2
  49. package/lib/locales/zh.js +20 -2
  50. package/lib/payment/form/address.js +6 -5
  51. package/lib/payment/form/index.js +49 -9
  52. package/lib/payment/index.js +20 -2
  53. package/lib/payment/product-item.d.ts +8 -1
  54. package/lib/payment/product-item.js +144 -4
  55. package/lib/payment/summary.d.ts +3 -1
  56. package/lib/payment/summary.js +9 -0
  57. package/package.json +3 -3
  58. package/src/components/blockchain/tx.tsx +9 -15
  59. package/src/components/country-select.tsx +2 -2
  60. package/src/components/date-range-picker.tsx +310 -0
  61. package/src/components/input.tsx +14 -3
  62. package/src/components/label.tsx +59 -0
  63. package/src/components/loading-button.tsx +1 -1
  64. package/src/history/credit/grants-list.tsx +276 -0
  65. package/src/history/credit/transactions-list.tsx +316 -0
  66. package/src/history/invoice/list.tsx +18 -1
  67. package/src/index.ts +9 -0
  68. package/src/libs/util.ts +14 -0
  69. package/src/locales/en.tsx +20 -0
  70. package/src/locales/zh.tsx +19 -0
  71. package/src/payment/form/address.tsx +4 -3
  72. package/src/payment/form/index.tsx +112 -53
  73. package/src/payment/index.tsx +17 -1
  74. package/src/payment/product-item.tsx +152 -4
  75. package/src/payment/summary.tsx +13 -2
@@ -7,12 +7,14 @@ module.exports = ProductItem;
7
7
  var _jsxRuntime = require("react/jsx-runtime");
8
8
  var _context = require("@arcblock/ux/lib/Locale/context");
9
9
  var _material = require("@mui/material");
10
+ var _iconsMaterial = require("@mui/icons-material");
10
11
  var _react = require("react");
11
12
  var _status = _interopRequireDefault(require("../components/status"));
12
13
  var _switchButton = _interopRequireDefault(require("../components/switch-button"));
13
14
  var _util = require("../libs/util");
14
15
  var _productCard = _interopRequireDefault(require("./product-card"));
15
16
  var _dayjs = _interopRequireDefault(require("../libs/dayjs"));
17
+ var _payment = require("../contexts/payment");
16
18
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
17
19
  function ProductItem({
18
20
  item,
@@ -23,12 +25,20 @@ function ProductItem({
23
25
  mode = "normal",
24
26
  children = null,
25
27
  onUpsell,
26
- onDownsell
28
+ onDownsell,
29
+ completed = false,
30
+ adjustableQuantity = {
31
+ enabled: false
32
+ },
33
+ onQuantityChange = () => {}
27
34
  }) {
28
35
  const {
29
36
  t,
30
37
  locale
31
38
  } = (0, _context.useLocaleContext)();
39
+ const {
40
+ settings
41
+ } = (0, _payment.usePaymentContext)();
32
42
  const pricing = (0, _util.formatLineItemPricing)(item, currency, {
33
43
  trialEnd,
34
44
  trialInDays
@@ -36,6 +46,65 @@ function ProductItem({
36
46
  const saving = (0, _util.formatUpsellSaving)(items, currency);
37
47
  const metered = item.price?.recurring?.usage_type === "metered" ? t("common.metered") : "";
38
48
  const canUpsell = mode === "normal" && items.length === 1;
49
+ const isCreditProduct = item.price.product?.type === "credit" && item.price.metadata?.credit_config?.credit_amount;
50
+ const creditAmount = isCreditProduct ? Number(item.price.metadata.credit_config.credit_amount) : 0;
51
+ const creditCurrency = isCreditProduct ? (0, _util.findCurrency)(settings.paymentMethods, item.price.metadata?.credit_config?.currency_id ?? "") : null;
52
+ const validDuration = item.price.metadata?.credit_config?.valid_duration_value;
53
+ const validDurationUnit = item.price.metadata?.credit_config?.valid_duration_unit || "days";
54
+ const [localQuantity, setLocalQuantity] = (0, _react.useState)(item.quantity);
55
+ const canAdjustQuantity = adjustableQuantity.enabled && mode === "normal";
56
+ const minQuantity = Math.max(adjustableQuantity.minimum || 1, 1);
57
+ const quantityAvailable = Math.min(item.price.quantity_limit_per_checkout, item.price.quantity_available);
58
+ const maxQuantity = Math.min(adjustableQuantity.maximum || 999, quantityAvailable || 999);
59
+ const handleQuantityChange = newQuantity => {
60
+ if (newQuantity >= minQuantity && newQuantity <= maxQuantity) {
61
+ setLocalQuantity(newQuantity);
62
+ if ((0, _util.formatQuantityInventory)(item.price, newQuantity, locale)) {
63
+ return;
64
+ }
65
+ onQuantityChange(item.price_id, newQuantity);
66
+ }
67
+ };
68
+ const handleQuantityIncrease = () => {
69
+ if (localQuantity < maxQuantity) {
70
+ handleQuantityChange(localQuantity + 1);
71
+ }
72
+ };
73
+ const handleQuantityDecrease = () => {
74
+ if (localQuantity > minQuantity) {
75
+ handleQuantityChange(localQuantity - 1);
76
+ }
77
+ };
78
+ const handleQuantityInputChange = event => {
79
+ const value = parseInt(event.target.value, 10);
80
+ if (!Number.isNaN(value)) {
81
+ handleQuantityChange(value);
82
+ }
83
+ };
84
+ const formatCreditInfo = () => {
85
+ if (!isCreditProduct) return null;
86
+ const isRecurring = item.price.type === "recurring";
87
+ const totalCredit = creditAmount * localQuantity;
88
+ let message = "";
89
+ if (isRecurring) {
90
+ message = t("payment.checkout.credit.recurringInfo", {
91
+ amount: totalCredit,
92
+ period: (0, _util.formatRecurring)(item.price.recurring, true, "per", locale)
93
+ });
94
+ } else {
95
+ message = t("payment.checkout.credit.oneTimeInfo", {
96
+ amount: totalCredit,
97
+ symbol: creditCurrency?.symbol || "Credits"
98
+ });
99
+ }
100
+ if (validDuration && validDuration > 0) {
101
+ message += `\uFF0C${t("payment.checkout.credit.expiresIn", {
102
+ duration: validDuration,
103
+ unit: t(`common.${validDurationUnit}`)
104
+ })}`;
105
+ }
106
+ return message;
107
+ };
39
108
  const primaryText = (0, _react.useMemo)(() => {
40
109
  const price = item.upsell_price || item.price || {};
41
110
  const isRecurring = price?.type === "recurring" && price?.recurring;
@@ -45,6 +114,7 @@ function ProductItem({
45
114
  }
46
115
  return pricing.primary;
47
116
  }, [trialInDays, trialEnd, pricing, item, locale]);
117
+ const quantityInventoryError = (0, _util.formatQuantityInventory)(item.price, localQuantity, locale);
48
118
  return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
49
119
  direction: "column",
50
120
  spacing: 1,
@@ -103,8 +173,8 @@ function ProductItem({
103
173
  children: pricing.secondary
104
174
  })]
105
175
  })]
106
- }), (0, _util.formatQuantityInventory)(item.price, item.quantity, locale) ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_status.default, {
107
- label: (0, _util.formatQuantityInventory)(item.price, item.quantity, locale),
176
+ }), quantityInventoryError ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_status.default, {
177
+ label: quantityInventoryError,
108
178
  variant: "outlined",
109
179
  sx: {
110
180
  mt: 1,
@@ -112,7 +182,77 @@ function ProductItem({
112
182
  backgroundColor: "chip.error.background",
113
183
  color: "chip.error.text"
114
184
  }
115
- }) : null, children]
185
+ }) : null, canAdjustQuantity && !completed && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
186
+ sx: {
187
+ mt: 1,
188
+ p: 1
189
+ },
190
+ children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
191
+ direction: "row",
192
+ spacing: 1,
193
+ sx: {
194
+ alignItems: "center"
195
+ },
196
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
197
+ variant: "body2",
198
+ sx: {
199
+ color: "text.secondary",
200
+ minWidth: "fit-content"
201
+ },
202
+ children: [t("common.quantity"), ":"]
203
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
204
+ size: "small",
205
+ onClick: handleQuantityDecrease,
206
+ disabled: localQuantity <= minQuantity,
207
+ sx: {
208
+ minWidth: 32,
209
+ width: 32,
210
+ height: 32
211
+ },
212
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.Remove, {
213
+ fontSize: "small"
214
+ })
215
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.TextField, {
216
+ size: "small",
217
+ value: localQuantity,
218
+ onChange: handleQuantityInputChange,
219
+ sx: {
220
+ width: 60
221
+ },
222
+ type: "number",
223
+ slotProps: {
224
+ htmlInput: {
225
+ min: minQuantity,
226
+ max: maxQuantity,
227
+ style: {
228
+ textAlign: "center",
229
+ padding: "4px"
230
+ }
231
+ }
232
+ }
233
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
234
+ size: "small",
235
+ onClick: handleQuantityIncrease,
236
+ disabled: localQuantity >= maxQuantity,
237
+ sx: {
238
+ minWidth: 32,
239
+ width: 32,
240
+ height: 32
241
+ },
242
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.Add, {
243
+ fontSize: "small"
244
+ })
245
+ })]
246
+ })
247
+ }), isCreditProduct && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, {
248
+ severity: "info",
249
+ sx: {
250
+ mt: 1,
251
+ fontSize: "0.875rem"
252
+ },
253
+ icon: false,
254
+ children: formatCreditInfo()
255
+ }), children]
116
256
  }), canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
117
257
  direction: "row",
118
258
  className: "product-item-upsell",
@@ -8,6 +8,7 @@ type Props = {
8
8
  showStaking?: boolean;
9
9
  onUpsell?: Function;
10
10
  onDownsell?: Function;
11
+ onQuantityChange?: Function;
11
12
  onChangeAmount?: Function;
12
13
  onApplyCrossSell?: Function;
13
14
  onCancelCrossSell?: Function;
@@ -15,6 +16,7 @@ type Props = {
15
16
  crossSellBehavior?: string;
16
17
  donationSettings?: DonationSettings;
17
18
  action?: string;
19
+ completed?: boolean;
18
20
  };
19
- export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, ...rest }: Props): import("react").JSX.Element;
21
+ export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, ...rest }: Props): import("react").JSX.Element;
20
22
  export {};
@@ -90,6 +90,7 @@ function PaymentSummary({
90
90
  billingThreshold,
91
91
  onUpsell = _noop.default,
92
92
  onDownsell = _noop.default,
93
+ onQuantityChange = _noop.default,
93
94
  onApplyCrossSell = _noop.default,
94
95
  onCancelCrossSell = _noop.default,
95
96
  onChangeAmount = _noop.default,
@@ -99,6 +100,7 @@ function PaymentSummary({
99
100
  donationSettings = void 0,
100
101
  action = "",
101
102
  trialEnd = 0,
103
+ completed = false,
102
104
  ...rest
103
105
  }) {
104
106
  const {
@@ -138,6 +140,10 @@ function PaymentSummary({
138
140
  await onUpsell(from, to);
139
141
  runAsync();
140
142
  };
143
+ const handleQuantityChange = async (itemId, quantity) => {
144
+ await onQuantityChange(itemId, quantity);
145
+ runAsync();
146
+ };
141
147
  const handleDownsell = async from => {
142
148
  await onDownsell(from);
143
149
  runAsync();
@@ -196,6 +202,9 @@ function PaymentSummary({
196
202
  currency,
197
203
  onUpsell: handleUpsell,
198
204
  onDownsell: handleDownsell,
205
+ adjustableQuantity: x.adjustable_quantity,
206
+ completed,
207
+ onQuantityChange: handleQuantityChange,
199
208
  children: x.cross_sell && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
200
209
  direction: "row",
201
210
  sx: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
4
4
  "description": "Reusable react components for payment kit v2",
5
5
  "keywords": [
6
6
  "react",
@@ -94,7 +94,7 @@
94
94
  "@babel/core": "^7.27.4",
95
95
  "@babel/preset-env": "^7.27.2",
96
96
  "@babel/preset-react": "^7.27.1",
97
- "@blocklet/payment-types": "1.19.0",
97
+ "@blocklet/payment-types": "1.19.2",
98
98
  "@storybook/addon-essentials": "^7.6.20",
99
99
  "@storybook/addon-interactions": "^7.6.20",
100
100
  "@storybook/addon-links": "^7.6.20",
@@ -125,5 +125,5 @@
125
125
  "vite-plugin-babel": "^1.3.1",
126
126
  "vite-plugin-node-polyfills": "^0.23.0"
127
127
  },
128
- "gitHead": "2810263174bb900ebb5a431dc53930b08b4a8d6d"
128
+ "gitHead": "741c897204afc412721a942201516932bff59235"
129
129
  }
@@ -6,24 +6,20 @@ import { styled } from '@mui/system';
6
6
 
7
7
  import { getTxLink } from '../../libs/util';
8
8
 
9
- export default function TxLink(rawProps: {
9
+ export default function TxLink({
10
+ details,
11
+ method,
12
+ mode = 'dashboard',
13
+ align = 'left',
14
+ }: {
10
15
  details: PaymentDetails;
11
16
  method: TPaymentMethod;
12
17
  mode?: 'customer' | 'dashboard';
13
18
  align?: 'left' | 'right';
14
19
  }) {
15
- const props = Object.assign(
16
- {
17
- mode: 'dashboard',
18
- align: 'left',
19
- },
20
- rawProps
21
- );
22
-
23
20
  const { t } = useLocaleContext();
24
21
 
25
- // eslint-disable-next-line react/prop-types
26
- if (!props.details || (props.mode === 'customer' && props.method.type === 'stripe')) {
22
+ if (!details || (mode === 'customer' && method.type === 'stripe')) {
27
23
  return (
28
24
  <Typography
29
25
  component="small"
@@ -35,8 +31,7 @@ export default function TxLink(rawProps: {
35
31
  );
36
32
  }
37
33
 
38
- // eslint-disable-next-line react/prop-types
39
- const { text, link } = getTxLink(props.method, props.details);
34
+ const { text, link } = getTxLink(method, details);
40
35
 
41
36
  if (link && text) {
42
37
  return (
@@ -44,8 +39,7 @@ export default function TxLink(rawProps: {
44
39
  <Root
45
40
  direction="row"
46
41
  alignItems="center"
47
- // eslint-disable-next-line react/prop-types
48
- justifyContent={props.align === 'left' ? 'flex-start' : 'flex-end'}
42
+ justifyContent={align === 'left' ? 'flex-start' : 'flex-end'}
49
43
  sx={{ color: 'text.link' }}
50
44
  spacing={1}>
51
45
  <Typography component="span" sx={{ color: 'text.link' }}>
@@ -7,7 +7,7 @@ import type { SlideProps, SxProps } from '@mui/material';
7
7
  import type { CountryIso2 } from 'react-international-phone';
8
8
  import { useMobile } from '../hooks/mobile';
9
9
 
10
- function Transition({ ref, ...props }: { ref: React.RefObject<unknown> } & SlideProps) {
10
+ function Transition({ ref, ...props }: { ref: React.RefObject<unknown | null> } & SlideProps) {
11
11
  return <Slide direction="up" ref={ref} timeout={200} {...props} />;
12
12
  }
13
13
 
@@ -27,7 +27,7 @@ export default function CountrySelect({
27
27
  sx = {},
28
28
  showDialCode = false,
29
29
  }: CountrySelectProps & {
30
- ref?: React.RefObject<HTMLDivElement>;
30
+ ref?: React.RefObject<HTMLDivElement | null>;
31
31
  }) {
32
32
  const { setValue } = useFormContext();
33
33
  const [open, setOpen] = useState(false);
@@ -0,0 +1,310 @@
1
+ /* eslint-disable @typescript-eslint/indent */
2
+ import { useState, useRef } from 'react';
3
+ import {
4
+ TextField,
5
+ Stack,
6
+ Typography,
7
+ useMediaQuery,
8
+ useTheme,
9
+ Popover,
10
+ Dialog,
11
+ DialogTitle,
12
+ DialogContent,
13
+ DialogActions,
14
+ Button,
15
+ Box,
16
+ IconButton,
17
+ } from '@mui/material';
18
+ import { DateRange, Close, Clear } from '@mui/icons-material';
19
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
20
+ import FormLabel from './label';
21
+ import { formatToDate } from '../libs/util';
22
+
23
+ export interface DateRangeValue {
24
+ start: number | undefined;
25
+ end: number | undefined;
26
+ }
27
+
28
+ export interface DateRangePickerProps {
29
+ value: DateRangeValue;
30
+ onChange: (value: DateRangeValue) => void;
31
+ label?: string;
32
+ size?: 'small' | 'medium';
33
+ fullWidth?: boolean;
34
+ disabled?: boolean;
35
+ }
36
+
37
+ interface DatePickerContentProps {
38
+ tempValue: { startDate: string; endDate: string };
39
+ setTempValue: (
40
+ value:
41
+ | { startDate: string; endDate: string }
42
+ | ((prev: { startDate: string; endDate: string }) => { startDate: string; endDate: string })
43
+ ) => void;
44
+ handleCancel: () => void;
45
+ handleApply: () => void;
46
+ handleClear: () => void;
47
+ }
48
+
49
+ // 日期选择器内容组件
50
+ function DatePickerContent({
51
+ tempValue,
52
+ setTempValue,
53
+ handleCancel,
54
+ handleApply,
55
+ handleClear,
56
+ }: DatePickerContentProps) {
57
+ const { t } = useLocaleContext();
58
+ return (
59
+ <Box sx={{ p: 2, minWidth: 320 }}>
60
+ <Stack spacing={2}>
61
+ <Box>
62
+ <FormLabel>{t('common.startDate')}</FormLabel>
63
+ <TextField
64
+ type="date"
65
+ value={tempValue.startDate}
66
+ onChange={(e) => setTempValue((prev) => ({ ...prev, startDate: e.target.value }))}
67
+ size="small"
68
+ fullWidth
69
+ slotProps={{
70
+ inputLabel: { shrink: true },
71
+ }}
72
+ />
73
+ </Box>
74
+ <Box>
75
+ <FormLabel>{t('common.endDate')}</FormLabel>
76
+ <TextField
77
+ type="date"
78
+ value={tempValue.endDate}
79
+ onChange={(e) => setTempValue((prev) => ({ ...prev, endDate: e.target.value }))}
80
+ size="small"
81
+ fullWidth
82
+ slotProps={{
83
+ inputLabel: { shrink: true },
84
+ }}
85
+ />
86
+ </Box>
87
+
88
+ <Stack
89
+ direction="row"
90
+ spacing={1}
91
+ sx={{
92
+ justifyContent: 'space-between',
93
+ }}>
94
+ <Button variant="text" onClick={handleClear} size="small" color="secondary" sx={{ px: 0.5, minWidth: 0 }}>
95
+ {t('common.clear')}
96
+ </Button>
97
+ <Stack direction="row" spacing={1}>
98
+ <Button variant="outlined" onClick={handleCancel} size="small">
99
+ {t('common.cancel')}
100
+ </Button>
101
+ <Button variant="contained" onClick={handleApply} size="small">
102
+ {t('common.confirm')}
103
+ </Button>
104
+ </Stack>
105
+ </Stack>
106
+ </Stack>
107
+ </Box>
108
+ );
109
+ }
110
+
111
+ const DateFormat = 'YYYY-MM-DD';
112
+
113
+ export default function DateRangePicker({
114
+ value,
115
+ onChange,
116
+ label = '',
117
+ size = 'small',
118
+ fullWidth = false,
119
+ disabled = false,
120
+ }: DateRangePickerProps) {
121
+ const theme = useTheme();
122
+ const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
123
+ const [open, setOpen] = useState(false);
124
+ const { t, locale } = useLocaleContext();
125
+ // 内部使用字符串格式的临时值
126
+ const [tempValue, setTempValue] = useState<{ startDate: string; endDate: string }>({
127
+ startDate: value.start ? formatToDate(value.start * 1000, locale, DateFormat) : '',
128
+ endDate: value.end ? formatToDate(value.end * 1000, locale, DateFormat) : '',
129
+ });
130
+ const anchorRef = useRef<HTMLDivElement>(null);
131
+
132
+ const formatDisplayValue = (startUnix: number | undefined, endUnix: number | undefined) => {
133
+ if (!startUnix || !endUnix) return t('common.selectTimeRange');
134
+ const start = formatToDate(startUnix * 1000, locale, DateFormat);
135
+ const end = formatToDate(endUnix * 1000, locale, DateFormat);
136
+ return `${start} ~ ${end}`;
137
+ };
138
+
139
+ const handleOpen = () => {
140
+ if (disabled) return;
141
+ setTempValue({
142
+ startDate: value.start ? formatToDate(value.start * 1000, locale, DateFormat) : '',
143
+ endDate: value.end ? formatToDate(value.end * 1000, locale, DateFormat) : '',
144
+ });
145
+ setOpen(true);
146
+ };
147
+
148
+ const handleClose = () => {
149
+ setOpen(false);
150
+ };
151
+
152
+ const handleApply = () => {
153
+ const unixValue: DateRangeValue = {
154
+ start: tempValue.startDate ? Math.floor(new Date(`${tempValue.startDate}T00:00:00`).getTime() / 1000) : 0,
155
+ end: tempValue.endDate ? Math.floor(new Date(`${tempValue.endDate}T23:59:59`).getTime() / 1000) : 0,
156
+ };
157
+ onChange(unixValue);
158
+ setOpen(false);
159
+ };
160
+
161
+ const handleCancel = () => {
162
+ setTempValue({
163
+ startDate: value.start ? formatToDate(value.start * 1000, locale, DateFormat) : '',
164
+ endDate: value.end ? formatToDate(value.end * 1000, locale, DateFormat) : '',
165
+ });
166
+ setOpen(false);
167
+ };
168
+
169
+ const handleClear = () => {
170
+ const emptyValue: DateRangeValue = { start: undefined, end: undefined };
171
+ onChange(emptyValue);
172
+ setTempValue({ startDate: '', endDate: '' });
173
+ setOpen(false);
174
+ };
175
+
176
+ const hasValue = !!value.start && !!value.end;
177
+
178
+ return (
179
+ <>
180
+ <TextField
181
+ ref={anchorRef}
182
+ label={label}
183
+ value={formatDisplayValue(value.start, value.end)}
184
+ size={size}
185
+ fullWidth={fullWidth}
186
+ disabled={disabled}
187
+ sx={{
188
+ '& .MuiInputBase-input': {
189
+ cursor: disabled ? 'default' : 'pointer',
190
+ },
191
+ }}
192
+ onClick={handleOpen}
193
+ placeholder={t('common.selectTimeRange')}
194
+ slotProps={{
195
+ input: {
196
+ readOnly: true,
197
+ startAdornment: <DateRange fontSize="small" sx={{ mr: 1, color: 'text.secondary' }} />,
198
+ endAdornment: hasValue && !disabled && (
199
+ <IconButton
200
+ size="small"
201
+ onClick={(e) => {
202
+ e.stopPropagation();
203
+ handleClear();
204
+ }}
205
+ sx={{
206
+ color: 'text.secondary',
207
+
208
+ '&:hover': { color: 'text.primary' },
209
+ }}>
210
+ <Clear fontSize="small" />
211
+ </IconButton>
212
+ ),
213
+ },
214
+
215
+ inputLabel: { shrink: true },
216
+ }}
217
+ />
218
+ {/* 桌面端使用 Popover */}
219
+ {!isMobile && (
220
+ <Popover
221
+ open={open}
222
+ anchorEl={anchorRef.current}
223
+ onClose={handleClose}
224
+ anchorOrigin={{
225
+ vertical: 'bottom',
226
+ horizontal: 'left',
227
+ }}
228
+ transformOrigin={{
229
+ vertical: 'top',
230
+ horizontal: 'left',
231
+ }}
232
+ sx={{
233
+ '& .MuiPaper-root': {
234
+ boxShadow: theme.shadows[8],
235
+ border: `1px solid ${theme.palette.divider}`,
236
+ },
237
+ }}>
238
+ <DatePickerContent
239
+ tempValue={tempValue}
240
+ setTempValue={setTempValue}
241
+ handleCancel={handleCancel}
242
+ handleApply={handleApply}
243
+ handleClear={handleClear}
244
+ />
245
+ </Popover>
246
+ )}
247
+ {/* 移动端使用 Dialog */}
248
+ {isMobile && (
249
+ <Dialog
250
+ open={open}
251
+ onClose={handleClose}
252
+ fullWidth
253
+ maxWidth="sm"
254
+ PaperProps={{
255
+ sx: {
256
+ m: 1,
257
+ maxHeight: 'calc(100% - 64px)',
258
+ },
259
+ }}>
260
+ <DialogTitle
261
+ sx={{
262
+ display: 'flex',
263
+ justifyContent: 'space-between',
264
+ alignItems: 'center',
265
+ pb: 1,
266
+ }}>
267
+ <Typography variant="h6">{t('common.selectTimeRange')}</Typography>
268
+ <IconButton edge="end" color="inherit" onClick={handleClose} aria-label="close" size="small">
269
+ <Close />
270
+ </IconButton>
271
+ </DialogTitle>
272
+ <DialogContent sx={{ pb: 1 }}>
273
+ <Stack spacing={2} sx={{ mt: 1 }}>
274
+ <FormLabel>{t('common.startDate')}</FormLabel>
275
+ <TextField
276
+ type="date"
277
+ value={tempValue.startDate}
278
+ onChange={(e) => setTempValue((prev) => ({ ...prev, startDate: e.target.value }))}
279
+ size="small"
280
+ fullWidth
281
+ slotProps={{
282
+ inputLabel: { shrink: true },
283
+ }}
284
+ />
285
+ <FormLabel>{t('common.endDate')}</FormLabel>
286
+ <TextField
287
+ type="date"
288
+ value={tempValue.endDate}
289
+ onChange={(e) => setTempValue((prev) => ({ ...prev, endDate: e.target.value }))}
290
+ size="small"
291
+ fullWidth
292
+ slotProps={{
293
+ inputLabel: { shrink: true },
294
+ }}
295
+ />
296
+ </Stack>
297
+ </DialogContent>
298
+ <DialogActions sx={{ px: 3, pb: 2 }}>
299
+ <Button onClick={handleCancel} color="inherit">
300
+ {t('common.cancel')}
301
+ </Button>
302
+ <Button onClick={handleApply} variant="contained">
303
+ {t('common.confirm')}
304
+ </Button>
305
+ </DialogActions>
306
+ </Dialog>
307
+ )}
308
+ </>
309
+ );
310
+ }