@arcblock/ux 2.13.46 → 2.13.48

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.
@@ -1,3 +1,4 @@
1
+ import { TooltipProps } from '@mui/material';
1
2
  import 'dayjs/locale/zh-cn';
2
3
  import type { Locale } from '../type';
3
4
  export interface RelativeTimeProps {
@@ -12,5 +13,6 @@ export interface RelativeTimeProps {
12
13
  enableTooltip?: boolean;
13
14
  useShortTimezone?: boolean;
14
15
  disableTimezone?: boolean;
16
+ placement?: TooltipProps['placement'];
15
17
  }
16
- export default function RelativeTime({ value, locale, withoutSuffix, from, to, type, tz, relativeRange, enableTooltip, useShortTimezone, disableTimezone, ...rest }: RelativeTimeProps): import("react/jsx-runtime").JSX.Element;
18
+ export default function RelativeTime({ value, locale, withoutSuffix, from, to, type, tz, relativeRange, enableTooltip, useShortTimezone, disableTimezone, placement, ...rest }: RelativeTimeProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Tooltip } from '@mui/material';
3
- import { useState, useMemo } from 'react';
3
+ import { useEffect, useMemo } from 'react';
4
4
  import relativeTime from 'dayjs/plugin/relativeTime';
5
5
  import dayjs from 'dayjs';
6
6
  import 'dayjs/locale/zh-cn';
@@ -8,6 +8,7 @@ import localizedFormat from 'dayjs/plugin/localizedFormat';
8
8
  import utc from 'dayjs/plugin/utc';
9
9
  import timezone from 'dayjs/plugin/timezone';
10
10
  import updateLocale from 'dayjs/plugin/updateLocale';
11
+ import { create } from 'zustand';
11
12
  import { formatToDatetime, setDateTool } from '../Util';
12
13
  dayjs.extend(localizedFormat);
13
14
  dayjs.extend(utc);
@@ -48,6 +49,12 @@ const translations = {
48
49
  shortLocal: '当前时区'
49
50
  }
50
51
  };
52
+ const useUtcStore = create(set => ({
53
+ isUtc: false,
54
+ setIsUtc: isUtc => set({
55
+ isUtc
56
+ })
57
+ }));
51
58
  function useRelativeTime({
52
59
  value,
53
60
  locale = 'en',
@@ -63,7 +70,12 @@ function useRelativeTime({
63
70
  const sign = timeZoneOffset > 0 ? '-' : '+';
64
71
  const hoursOffset = Math.abs(timeZoneOffset) / 60;
65
72
  const isLocalUtc = timeZoneOffset === 0;
66
- const [isUtc, setIsUtc] = useState(isLocalUtc);
73
+ const isUtc = useUtcStore(state => state.isUtc);
74
+ const setIsUtc = useUtcStore(state => state.setIsUtc);
75
+ useEffect(() => {
76
+ setIsUtc(isLocalUtc);
77
+ // eslint-disable-next-line react-hooks/exhaustive-deps
78
+ }, [isLocalUtc]);
67
79
  if (!value) {
68
80
  return {
69
81
  innerContent: '-',
@@ -137,8 +149,6 @@ function useRelativeTime({
137
149
  function UTCChip({
138
150
  locale,
139
151
  isUtc,
140
- sign,
141
- hoursOffset,
142
152
  setIsUtc,
143
153
  useShortTimezone = true
144
154
  }) {
@@ -162,7 +172,7 @@ function UTCChip({
162
172
  padding: '4px 8px',
163
173
  lineHeight: 1
164
174
  },
165
- onClick: () => setIsUtc(r => !r),
175
+ onClick: () => setIsUtc(!isUtc),
166
176
  children: text
167
177
  });
168
178
  }
@@ -178,6 +188,7 @@ export default function RelativeTime({
178
188
  enableTooltip = true,
179
189
  useShortTimezone = false,
180
190
  disableTimezone = false,
191
+ placement = 'top-end',
181
192
  ...rest
182
193
  }) {
183
194
  const {
@@ -185,8 +196,6 @@ export default function RelativeTime({
185
196
  popContent,
186
197
  isUtc,
187
198
  setIsUtc,
188
- sign,
189
- hoursOffset,
190
199
  relativeString
191
200
  } = useRelativeTime({
192
201
  value,
@@ -201,7 +210,7 @@ export default function RelativeTime({
201
210
  if (type === 'all') {
202
211
  return /*#__PURE__*/_jsx(Tooltip, {
203
212
  title: undefined,
204
- placement: "top-end",
213
+ placement: placement,
205
214
  enterTouchDelay: 0,
206
215
  children: /*#__PURE__*/_jsxs(Box, {
207
216
  display: "inline-flex",
@@ -235,8 +244,6 @@ export default function RelativeTime({
235
244
  }), /*#__PURE__*/_jsx(UTCChip, {
236
245
  locale: locale,
237
246
  isUtc: isUtc,
238
- sign: sign,
239
- hoursOffset: hoursOffset,
240
247
  setIsUtc: setIsUtc,
241
248
  useShortTimezone: useShortTimezone
242
249
  })]
@@ -246,7 +253,7 @@ export default function RelativeTime({
246
253
  }
247
254
  return /*#__PURE__*/_jsx(Tooltip, {
248
255
  title: enableTooltip ? popContent : undefined,
249
- placement: "top-end",
256
+ placement: placement,
250
257
  enterTouchDelay: 0,
251
258
  children: /*#__PURE__*/_jsxs(Box, {
252
259
  display: "inline-flex",
@@ -259,8 +266,6 @@ export default function RelativeTime({
259
266
  }), type === 'utc' && !disableTimezone && /*#__PURE__*/_jsx(UTCChip, {
260
267
  locale: locale,
261
268
  isUtc: isUtc,
262
- sign: sign,
263
- hoursOffset: hoursOffset,
264
269
  setIsUtc: setIsUtc,
265
270
  useShortTimezone: useShortTimezone
266
271
  })]
@@ -7,6 +7,7 @@ import StyledEngineProvider from '@mui/material/StyledEngineProvider';
7
7
  import CssBaseline from '@mui/material/CssBaseline';
8
8
  import set from 'lodash/set';
9
9
  import { BLOCKLET_THEME_PREFER_KEY } from '@blocklet/theme';
10
+ import useLocationState from '../hooks/use-location-state';
10
11
  import { createTheme, getDefaultThemePrefer, isTheme, isUxTheme, lazyCreateDefaultTheme } from './theme';
11
12
  const defaultTheme = createTheme();
12
13
 
@@ -19,9 +20,9 @@ export function useColorScheme() {
19
20
 
20
21
  /** 根据偏好获取颜色模式 */
21
22
  const resolveMode = prefer => {
23
+ // 允许组件的 prefer 属性覆盖 blocklet theme 中配置的 prefer
22
24
  if (prefer) {
23
25
  if (prefer === 'system') {
24
- // 取系统默认
25
26
  return getDefaultThemePrefer({
26
27
  theme: {
27
28
  prefer: 'system'
@@ -123,6 +124,7 @@ function ColorSchemeProvider({
123
124
  }) {
124
125
  const [mode, setMode] = useState(() => resolveMode(prefer));
125
126
  const parentTheme = useTheme();
127
+ const location = useLocationState();
126
128
  const _themeInput = useMemo(() => {
127
129
  let result = {};
128
130
  const createBaseTheme = lazyCreateDefaultTheme(mode);
@@ -177,11 +179,11 @@ function ColorSchemeProvider({
177
179
  changeMode,
178
180
  prefer
179
181
  }), [mode, prefer, toggleMode, changeMode]);
182
+
183
+ // 监听 prefer 或者 url.search 变化
180
184
  useEffect(() => {
181
- if (prefer) {
182
- setMode(resolveMode(prefer));
183
- }
184
- }, [prefer, setMode]);
185
+ setMode(resolveMode(prefer));
186
+ }, [prefer, setMode, location.search]);
185
187
  return /*#__PURE__*/_jsx(ColorSchemeContext.Provider, {
186
188
  value: colorSchemeValue,
187
189
  children: /*#__PURE__*/_jsx(BaseThemeProvider, {
@@ -73,9 +73,15 @@ export function loadFonts(fonts) {
73
73
 
74
74
  // 获取默认主题偏好
75
75
  export function getDefaultThemePrefer(meta) {
76
+ // 跟随 url theme 参数
77
+ const urlParams = new URLSearchParams(window.location.search);
78
+ const urlPrefer = urlParams.get('theme');
79
+ if (urlPrefer === 'light' || urlPrefer === 'dark') {
80
+ return urlPrefer;
81
+ }
76
82
  const prefer = Object.assign({}, window.blocklet, meta).theme?.prefer;
77
83
  if (prefer === 'system') {
78
- // 本地缓存
84
+ // 跟随本地缓存
79
85
  const localPrefer = localStorage.getItem(BLOCKLET_THEME_PREFER_KEY);
80
86
  if (localPrefer && (localPrefer === 'light' || localPrefer === 'dark')) {
81
87
  return localPrefer;
@@ -84,11 +90,12 @@ export function getDefaultThemePrefer(meta) {
84
90
  // 跟随系统
85
91
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
86
92
  }
93
+ // 跟随 blocklet theme mode
87
94
  if (prefer === 'light' || prefer === 'dark') {
88
95
  return prefer;
89
96
  }
90
97
 
91
- // 未设置偏好
98
+ // fallback
92
99
  return 'light';
93
100
  }
94
101
 
@@ -0,0 +1,16 @@
1
+ export interface LocationLike {
2
+ href: string;
3
+ origin: string;
4
+ host: string;
5
+ hostname: string;
6
+ port: string;
7
+ protocol: string;
8
+ pathname: string;
9
+ search: string;
10
+ hash: string;
11
+ }
12
+ /**
13
+ * 监听 location 变化,返回当前 location 状态
14
+ */
15
+ export declare function useLocationState(): LocationLike;
16
+ export default useLocationState;
@@ -0,0 +1,89 @@
1
+ /* eslint-disable no-restricted-globals */
2
+ import { useEffect, useState } from 'react';
3
+ const subscribers = new Set();
4
+ let patched = false;
5
+ let lastHref = '';
6
+
7
+ // 检查是否在浏览器环境中
8
+ const isBrowser = typeof window !== 'undefined';
9
+ function notifyIfChanged() {
10
+ if (!isBrowser) return;
11
+ const currentHref = window.location.href;
12
+ if (currentHref !== lastHref) {
13
+ lastHref = currentHref;
14
+ subscribers.forEach(cb => cb(window.location));
15
+ }
16
+ }
17
+ function patchHistoryOnce() {
18
+ if (!isBrowser || patched) return;
19
+ patched = true;
20
+ const originalPushState = history.pushState;
21
+ history.pushState = function patchedPushState(...args) {
22
+ const result = originalPushState.apply(this, args);
23
+ notifyIfChanged();
24
+ return result;
25
+ };
26
+ const originalReplaceState = history.replaceState;
27
+ history.replaceState = function patchedReplaceState(...args) {
28
+ const result = originalReplaceState.apply(this, args);
29
+ notifyIfChanged();
30
+ return result;
31
+ };
32
+ window.addEventListener('popstate', notifyIfChanged);
33
+ window.addEventListener('hashchange', notifyIfChanged);
34
+ }
35
+ function subscribeUrlChange(callback) {
36
+ if (isBrowser) {
37
+ patchHistoryOnce();
38
+ subscribers.add(callback);
39
+ }
40
+ return () => {
41
+ subscribers.delete(callback);
42
+ };
43
+ }
44
+ function extractLocation(location) {
45
+ return {
46
+ href: location.href,
47
+ origin: location.origin,
48
+ host: location.host,
49
+ hostname: location.hostname,
50
+ pathname: location.pathname,
51
+ port: location.port,
52
+ protocol: location.protocol,
53
+ search: location.search,
54
+ hash: location.hash
55
+ };
56
+ }
57
+ const defaultLocation = {
58
+ href: '',
59
+ origin: '',
60
+ host: '',
61
+ hostname: '',
62
+ port: '',
63
+ protocol: '',
64
+ pathname: '',
65
+ search: '',
66
+ hash: ''
67
+ };
68
+
69
+ /**
70
+ * 监听 location 变化,返回当前 location 状态
71
+ */
72
+ export function useLocationState() {
73
+ const [location, setLocation] = useState(() => {
74
+ if (!isBrowser) {
75
+ return defaultLocation;
76
+ }
77
+ return extractLocation(window.location);
78
+ });
79
+ useEffect(() => {
80
+ if (!isBrowser) {
81
+ return undefined;
82
+ }
83
+ return subscribeUrlChange(newLocation => {
84
+ setLocation(extractLocation(newLocation));
85
+ });
86
+ }, []);
87
+ return location;
88
+ }
89
+ export default useLocationState;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcblock/ux",
3
- "version": "2.13.46",
3
+ "version": "2.13.48",
4
4
  "description": "Common used react components for arcblock products",
5
5
  "keywords": [
6
6
  "react",
@@ -71,14 +71,14 @@
71
71
  "react": ">=18.2.0",
72
72
  "react-router-dom": ">=6.22.3"
73
73
  },
74
- "gitHead": "1e351b068cb3856aa4c8b1bf9f0067d95e8aec17",
74
+ "gitHead": "e6639ffcaa28acbebef892a278fe609fc24ee365",
75
75
  "dependencies": {
76
76
  "@arcblock/did-motif": "^1.1.13",
77
- "@arcblock/icons": "^2.13.46",
78
- "@arcblock/nft-display": "^2.13.46",
79
- "@arcblock/react-hooks": "^2.13.46",
77
+ "@arcblock/icons": "^2.13.48",
78
+ "@arcblock/nft-display": "^2.13.48",
79
+ "@arcblock/react-hooks": "^2.13.48",
80
80
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
81
- "@blocklet/theme": "^2.13.46",
81
+ "@blocklet/theme": "^2.13.48",
82
82
  "@fontsource/roboto": "~5.1.1",
83
83
  "@fontsource/ubuntu-mono": "^5.0.18",
84
84
  "@iconify-icons/logos": "^1.2.36",
@@ -129,6 +129,7 @@
129
129
  "type-fest": "^4.28.0",
130
130
  "validator": "^13.9.0",
131
131
  "versor": "^0.0.4",
132
- "webfontloader": "^1.6.28"
132
+ "webfontloader": "^1.6.28",
133
+ "zustand": "^5.0.5"
133
134
  }
134
135
  }
@@ -1,5 +1,5 @@
1
- import { Box, Tooltip } from '@mui/material';
2
- import { useState, useMemo } from 'react';
1
+ import { Box, Tooltip, TooltipProps } from '@mui/material';
2
+ import { useEffect, useMemo } from 'react';
3
3
  import relativeTime from 'dayjs/plugin/relativeTime';
4
4
  import dayjs from 'dayjs';
5
5
  import 'dayjs/locale/zh-cn';
@@ -7,6 +7,7 @@ import localizedFormat from 'dayjs/plugin/localizedFormat';
7
7
  import utc from 'dayjs/plugin/utc';
8
8
  import timezone from 'dayjs/plugin/timezone';
9
9
  import updateLocale from 'dayjs/plugin/updateLocale';
10
+ import { create } from 'zustand';
10
11
  import { formatToDatetime, setDateTool } from '../Util';
11
12
  import type { Locale } from '../type';
12
13
 
@@ -51,6 +52,12 @@ const translations: Record<Locale, { utc: string; local: string; shortUTC: strin
51
52
  },
52
53
  };
53
54
 
55
+ type UtcState = { isUtc: boolean; setIsUtc: (isUtc: boolean) => void };
56
+ const useUtcStore = create<UtcState>((set) => ({
57
+ isUtc: false,
58
+ setIsUtc: (isUtc: boolean) => set({ isUtc }),
59
+ }));
60
+
54
61
  export interface RelativeTimeProps {
55
62
  value: string | number;
56
63
  locale?: Locale;
@@ -63,6 +70,7 @@ export interface RelativeTimeProps {
63
70
  enableTooltip?: boolean;
64
71
  useShortTimezone?: boolean;
65
72
  disableTimezone?: boolean;
73
+ placement?: TooltipProps['placement'];
66
74
  }
67
75
 
68
76
  function useRelativeTime({
@@ -89,7 +97,14 @@ function useRelativeTime({
89
97
  const sign = timeZoneOffset > 0 ? '-' : '+';
90
98
  const hoursOffset = Math.abs(timeZoneOffset) / 60;
91
99
  const isLocalUtc = timeZoneOffset === 0;
92
- const [isUtc, setIsUtc] = useState(isLocalUtc);
100
+
101
+ const isUtc = useUtcStore((state) => state.isUtc);
102
+ const setIsUtc = useUtcStore((state) => state.setIsUtc);
103
+
104
+ useEffect(() => {
105
+ setIsUtc(isLocalUtc);
106
+ // eslint-disable-next-line react-hooks/exhaustive-deps
107
+ }, [isLocalUtc]);
93
108
 
94
109
  if (!value) {
95
110
  return { innerContent: '-', popContent: '-', isUtc, setIsUtc, sign, hoursOffset };
@@ -149,16 +164,12 @@ function useRelativeTime({
149
164
  function UTCChip({
150
165
  locale,
151
166
  isUtc,
152
- sign,
153
- hoursOffset,
154
167
  setIsUtc,
155
168
  useShortTimezone = true,
156
169
  }: {
157
170
  locale: Locale;
158
171
  isUtc?: boolean;
159
- sign: string;
160
- hoursOffset: number;
161
- setIsUtc: (data: any) => void;
172
+ setIsUtc: (data: boolean) => void;
162
173
  useShortTimezone?: boolean;
163
174
  }) {
164
175
  const text = useMemo(() => {
@@ -185,7 +196,7 @@ function UTCChip({
185
196
  padding: '4px 8px',
186
197
  lineHeight: 1,
187
198
  }}
188
- onClick={() => setIsUtc((r: any) => !r)}>
199
+ onClick={() => setIsUtc(!isUtc)}>
189
200
  {text}
190
201
  </Box>
191
202
  );
@@ -203,9 +214,10 @@ export default function RelativeTime({
203
214
  enableTooltip = true,
204
215
  useShortTimezone = false,
205
216
  disableTimezone = false,
217
+ placement = 'top-end',
206
218
  ...rest
207
219
  }: RelativeTimeProps) {
208
- const { innerContent, popContent, isUtc, setIsUtc, sign, hoursOffset, relativeString } = useRelativeTime({
220
+ const { innerContent, popContent, isUtc, setIsUtc, relativeString } = useRelativeTime({
209
221
  value,
210
222
  locale,
211
223
  withoutSuffix,
@@ -218,7 +230,7 @@ export default function RelativeTime({
218
230
 
219
231
  if (type === 'all') {
220
232
  return (
221
- <Tooltip title={undefined} placement="top-end" enterTouchDelay={0}>
233
+ <Tooltip title={undefined} placement={placement} enterTouchDelay={0}>
222
234
  <Box display="inline-flex" alignItems="center" gap={0.5} {...rest}>
223
235
  <Box component="span" {...rest} sx={{}}>
224
236
  {innerContent}
@@ -238,14 +250,7 @@ export default function RelativeTime({
238
250
  ·
239
251
  </Box>
240
252
 
241
- <UTCChip
242
- locale={locale}
243
- isUtc={isUtc}
244
- sign={sign}
245
- hoursOffset={hoursOffset}
246
- setIsUtc={setIsUtc}
247
- useShortTimezone={useShortTimezone}
248
- />
253
+ <UTCChip locale={locale} isUtc={isUtc} setIsUtc={setIsUtc} useShortTimezone={useShortTimezone} />
249
254
  </>
250
255
  )}
251
256
  </Box>
@@ -254,21 +259,14 @@ export default function RelativeTime({
254
259
  }
255
260
 
256
261
  return (
257
- <Tooltip title={enableTooltip ? popContent : undefined} placement="top-end" enterTouchDelay={0}>
262
+ <Tooltip title={enableTooltip ? popContent : undefined} placement={placement} enterTouchDelay={0}>
258
263
  <Box display="inline-flex" alignItems="center" gap={1}>
259
264
  <Box component="span" {...rest}>
260
265
  {innerContent}
261
266
  </Box>
262
267
 
263
268
  {type === 'utc' && !disableTimezone && (
264
- <UTCChip
265
- locale={locale}
266
- isUtc={isUtc}
267
- sign={sign}
268
- hoursOffset={hoursOffset}
269
- setIsUtc={setIsUtc}
270
- useShortTimezone={useShortTimezone}
271
- />
269
+ <UTCChip locale={locale} isUtc={isUtc} setIsUtc={setIsUtc} useShortTimezone={useShortTimezone} />
272
270
  )}
273
271
  </Box>
274
272
  </Tooltip>
@@ -7,6 +7,8 @@ import CssBaseline from '@mui/material/CssBaseline';
7
7
  import set from 'lodash/set';
8
8
  import { BLOCKLET_THEME_PREFER_KEY } from '@blocklet/theme';
9
9
 
10
+ import useLocationState from '../hooks/use-location-state';
11
+
10
12
  import {
11
13
  createTheme,
12
14
  getDefaultThemePrefer,
@@ -33,9 +35,9 @@ export function useColorScheme() {
33
35
 
34
36
  /** 根据偏好获取颜色模式 */
35
37
  const resolveMode = (prefer?: Prefer): PaletteMode => {
38
+ // 允许组件的 prefer 属性覆盖 blocklet theme 中配置的 prefer
36
39
  if (prefer) {
37
40
  if (prefer === 'system') {
38
- // 取系统默认
39
41
  return getDefaultThemePrefer({ theme: { prefer: 'system' } });
40
42
  }
41
43
  return prefer;
@@ -166,6 +168,7 @@ function ColorSchemeProvider({
166
168
  }: ThemeProviderProps) {
167
169
  const [mode, setMode] = useState<PaletteMode>(() => resolveMode(prefer));
168
170
  const parentTheme = useTheme();
171
+ const location = useLocationState();
169
172
 
170
173
  const _themeInput = useMemo(() => {
171
174
  let result: UxThemeOptions = {};
@@ -222,11 +225,10 @@ function ColorSchemeProvider({
222
225
  [mode, prefer, toggleMode, changeMode]
223
226
  );
224
227
 
228
+ // 监听 prefer 或者 url.search 变化
225
229
  useEffect(() => {
226
- if (prefer) {
227
- setMode(resolveMode(prefer));
228
- }
229
- }, [prefer, setMode]);
230
+ setMode(resolveMode(prefer));
231
+ }, [prefer, setMode, location.search]);
230
232
 
231
233
  return (
232
234
  <ColorSchemeContext.Provider value={colorSchemeValue}>
@@ -82,10 +82,16 @@ export function loadFonts(fonts: string[]) {
82
82
 
83
83
  // 获取默认主题偏好
84
84
  export function getDefaultThemePrefer(meta?: { theme: { prefer: 'light' | 'dark' | 'system' } }): PaletteMode {
85
- const prefer = Object.assign({}, window.blocklet, meta).theme?.prefer;
85
+ // 跟随 url theme 参数
86
+ const urlParams = new URLSearchParams(window.location.search);
87
+ const urlPrefer = urlParams.get('theme');
88
+ if (urlPrefer === 'light' || urlPrefer === 'dark') {
89
+ return urlPrefer;
90
+ }
86
91
 
92
+ const prefer = Object.assign({}, window.blocklet, meta).theme?.prefer;
87
93
  if (prefer === 'system') {
88
- // 本地缓存
94
+ // 跟随本地缓存
89
95
  const localPrefer = localStorage.getItem(BLOCKLET_THEME_PREFER_KEY) as PaletteMode;
90
96
  if (localPrefer && (localPrefer === 'light' || localPrefer === 'dark')) {
91
97
  return localPrefer;
@@ -94,12 +100,12 @@ export function getDefaultThemePrefer(meta?: { theme: { prefer: 'light' | 'dark'
94
100
  // 跟随系统
95
101
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
96
102
  }
97
-
103
+ // 跟随 blocklet theme mode
98
104
  if (prefer === 'light' || prefer === 'dark') {
99
105
  return prefer;
100
106
  }
101
107
 
102
- // 未设置偏好
108
+ // fallback
103
109
  return 'light';
104
110
  }
105
111
 
@@ -0,0 +1,117 @@
1
+ /* eslint-disable no-restricted-globals */
2
+ import { useEffect, useState } from 'react';
3
+
4
+ type UrlChangeCallback = (location: Location) => void;
5
+
6
+ const subscribers = new Set<UrlChangeCallback>();
7
+ let patched = false;
8
+ let lastHref = '';
9
+
10
+ // 检查是否在浏览器环境中
11
+ const isBrowser = typeof window !== 'undefined';
12
+
13
+ function notifyIfChanged() {
14
+ if (!isBrowser) return;
15
+
16
+ const currentHref = window.location.href;
17
+ if (currentHref !== lastHref) {
18
+ lastHref = currentHref;
19
+ subscribers.forEach((cb) => cb(window.location));
20
+ }
21
+ }
22
+
23
+ function patchHistoryOnce() {
24
+ if (!isBrowser || patched) return;
25
+ patched = true;
26
+
27
+ const originalPushState = history.pushState;
28
+ history.pushState = function patchedPushState(...args) {
29
+ const result = originalPushState.apply(this, args);
30
+ notifyIfChanged();
31
+ return result;
32
+ };
33
+
34
+ const originalReplaceState = history.replaceState;
35
+ history.replaceState = function patchedReplaceState(...args) {
36
+ const result = originalReplaceState.apply(this, args);
37
+ notifyIfChanged();
38
+ return result;
39
+ };
40
+
41
+ window.addEventListener('popstate', notifyIfChanged);
42
+ window.addEventListener('hashchange', notifyIfChanged);
43
+ }
44
+
45
+ function subscribeUrlChange(callback: UrlChangeCallback): () => void {
46
+ if (isBrowser) {
47
+ patchHistoryOnce();
48
+ subscribers.add(callback);
49
+ }
50
+ return () => {
51
+ subscribers.delete(callback);
52
+ };
53
+ }
54
+
55
+ export interface LocationLike {
56
+ href: string;
57
+ origin: string;
58
+ host: string;
59
+ hostname: string;
60
+ port: string;
61
+ protocol: string;
62
+ pathname: string;
63
+ search: string;
64
+ hash: string;
65
+ }
66
+
67
+ function extractLocation(location: Location): LocationLike {
68
+ return {
69
+ href: location.href,
70
+ origin: location.origin,
71
+ host: location.host,
72
+ hostname: location.hostname,
73
+ pathname: location.pathname,
74
+ port: location.port,
75
+ protocol: location.protocol,
76
+ search: location.search,
77
+ hash: location.hash,
78
+ };
79
+ }
80
+
81
+ const defaultLocation: LocationLike = {
82
+ href: '',
83
+ origin: '',
84
+ host: '',
85
+ hostname: '',
86
+ port: '',
87
+ protocol: '',
88
+ pathname: '',
89
+ search: '',
90
+ hash: '',
91
+ };
92
+
93
+ /**
94
+ * 监听 location 变化,返回当前 location 状态
95
+ */
96
+ export function useLocationState(): LocationLike {
97
+ const [location, setLocation] = useState<LocationLike>(() => {
98
+ if (!isBrowser) {
99
+ return defaultLocation;
100
+ }
101
+ return extractLocation(window.location);
102
+ });
103
+
104
+ useEffect(() => {
105
+ if (!isBrowser) {
106
+ return undefined;
107
+ }
108
+
109
+ return subscribeUrlChange((newLocation) => {
110
+ setLocation(extractLocation(newLocation));
111
+ });
112
+ }, []);
113
+
114
+ return location;
115
+ }
116
+
117
+ export default useLocationState;