@lobehub/chat 0.70.3 → 0.71.0
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/CHANGELOG.md +50 -0
- package/locales/en_US/common.json +2 -2
- package/locales/ru_RU/common.json +1 -1
- package/locales/zh_CN/common.json +1 -1
- package/package.json +8 -5
- package/src/features/MobileTabBar/index.tsx +1 -1
- package/src/locales/default/common.ts +1 -1
- package/src/pages/chat/features/Header/Mobile.tsx +1 -1
- package/src/pages/chat/setting/features/Header/Desktop.tsx +2 -4
- package/src/pages/chat/setting/features/Header/Mobile.tsx +1 -1
- package/src/pages/market/features/Header/Desktop.tsx +8 -32
- package/src/store/global/hooks/useHydrated.ts +3 -5
- package/src/store/global/hooks/useOnFinishHydrationGlobal.ts +3 -5
- package/src/store/middleware/createHyperStorage/index.ts +56 -42
- package/src/store/middleware/createHyperStorage/indexedDB.ts +26 -0
- package/src/store/middleware/createHyperStorage/localStorage.ts +17 -0
- package/src/store/middleware/createHyperStorage/type.ts +5 -0
- package/src/store/middleware/createHyperStorage/urlStorage.ts +60 -0
- package/src/store/session/hooks/useOnFinishHydrationSession.ts +3 -4
- package/src/store/session/hooks/useSessionHydrated.ts +4 -5
- package/src/store/session/store.ts +2 -0
- package/src/store/middleware/createHyperStorage/urlSearch.ts +0 -17
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 0.71.0](https://github.com/lobehub/lobe-chat/compare/v0.70.4...v0.71.0)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-09-09**</sup>
|
|
8
|
+
|
|
9
|
+
#### ✨ Features
|
|
10
|
+
|
|
11
|
+
- **misc**: Migrate localStorage to indexedDB.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's improved
|
|
19
|
+
|
|
20
|
+
- **misc**: Migrate localStorage to indexedDB, closes [#160](https://github.com/lobehub/lobe-chat/issues/160) ([7f96deb](https://github.com/lobehub/lobe-chat/commit/7f96deb))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 0.70.4](https://github.com/lobehub/lobe-chat/compare/v0.70.3...v0.70.4)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2023-09-09**</sup>
|
|
33
|
+
|
|
34
|
+
#### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **misc**: Fix route.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **misc**: Fix route ([2d1e8d6](https://github.com/lobehub/lobe-chat/commit/2d1e8d6))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
### [Version 0.70.3](https://github.com/lobehub/lobe-chat/compare/v0.70.2...v0.70.3)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2023-09-09**</sup>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"about": "About",
|
|
3
3
|
"advanceSettings": "Advanced Settings",
|
|
4
|
-
"agentDefaultMessage": "Hello, I'm **{{name}}**. You can start chatting with me right away or go to [Assistant Settings](/chat/{{id}}
|
|
4
|
+
"agentDefaultMessage": "Hello, I'm **{{name}}**. You can start chatting with me right away or go to [Assistant Settings](/chat/setting#session={{id}}) to complete my information.",
|
|
5
5
|
"agentDefaultMessageWithSystemRole": "Hello, I'm **{{name}}**, {{systemRole}}. Let's start chatting!",
|
|
6
6
|
"agentMaxToken": "Max Session Length",
|
|
7
7
|
"agentModel": "Model",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"stop": "Stop",
|
|
63
63
|
"tab": {
|
|
64
64
|
"chat": "Chat",
|
|
65
|
-
"market": "
|
|
65
|
+
"market": "Discover",
|
|
66
66
|
"setting": "Settings"
|
|
67
67
|
},
|
|
68
68
|
"temp": "Temporary",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"about": "Наш Github",
|
|
3
3
|
"advanceSettings": "Дополнительные настройки",
|
|
4
|
-
"agentDefaultMessage": "Здравствуйте, я **{{name}}**. Вы можете начать общаться со мной прямо сейчас или перейти на [Assistant Settings](/chat/{{id}}
|
|
4
|
+
"agentDefaultMessage": "Здравствуйте, я **{{name}}**. Вы можете начать общаться со мной прямо сейчас или перейти на [Assistant Settings](/chat/setting#session={{id}}) для моей настройки.",
|
|
5
5
|
"agentDefaultMessageWithSystemRole": "Здравствуйте, Я **{{name}}**, {{systemRole}}. Давайте начнем общаться!",
|
|
6
6
|
"agentMaxToken": "Максимальная длина запроса",
|
|
7
7
|
"agentModel": "Модель",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"about": "关于",
|
|
3
3
|
"advanceSettings": "高级设置",
|
|
4
|
-
"agentDefaultMessage": "你好,我是 **{{name}}**,你可以立即与我开始对话,也可以前往 [助手设置](/chat/{{id}}
|
|
4
|
+
"agentDefaultMessage": "你好,我是 **{{name}}**,你可以立即与我开始对话,也可以前往 [助手设置](/chat/setting#session={{id}}) 完善我的信息。",
|
|
5
5
|
"agentDefaultMessageWithSystemRole": "你好,我是 **{{name}}**,{{systemRole}},让我们开始对话吧!",
|
|
6
6
|
"agentMaxToken": "会话最大长度",
|
|
7
7
|
"agentModel": "模型",
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Lobe Chat is an open-source chatbot client using
|
|
3
|
+
"version": "0.71.0",
|
|
4
|
+
"description": "Lobe Chat is an open-source chatbot client using Vercel AI SDK, Typescript and Next.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatbot",
|
|
7
7
|
"ChatGPT",
|
|
8
|
-
"
|
|
8
|
+
"LobeHub",
|
|
9
9
|
"typescript",
|
|
10
|
-
"
|
|
10
|
+
"Vercel AI SDK",
|
|
11
|
+
"Next.js",
|
|
12
|
+
"OpenAI"
|
|
11
13
|
],
|
|
12
14
|
"homepage": "https://github.com/lobehub/lobe-chat",
|
|
13
15
|
"bugs": {
|
|
@@ -80,12 +82,13 @@
|
|
|
80
82
|
"fast-deep-equal": "^3",
|
|
81
83
|
"gpt-tokenizer": "^2",
|
|
82
84
|
"i18next": "^23",
|
|
85
|
+
"idb-keyval": "^6",
|
|
83
86
|
"immer": "^10",
|
|
84
87
|
"lodash-es": "^4",
|
|
85
88
|
"lucide-react": "latest",
|
|
86
89
|
"nanoid": "^4",
|
|
87
90
|
"next": "13.4.7",
|
|
88
|
-
"openai": "^4.
|
|
91
|
+
"openai": "^4.5.0",
|
|
89
92
|
"polished": "^4",
|
|
90
93
|
"react": "^18",
|
|
91
94
|
"react-dom": "^18",
|
|
@@ -36,7 +36,7 @@ export default memo<{ className?: string }>(({ className }) => {
|
|
|
36
36
|
icon: (active) => <Icon className={active ? styles.active : undefined} icon={Sticker} />,
|
|
37
37
|
key: 'market',
|
|
38
38
|
onClick: () => {
|
|
39
|
-
Router.push('
|
|
39
|
+
Router.push({ hash: '', pathname: `/market` });
|
|
40
40
|
},
|
|
41
41
|
title: t('tab.market'),
|
|
42
42
|
},
|
|
@@ -2,7 +2,7 @@ export default {
|
|
|
2
2
|
about: '关于',
|
|
3
3
|
advanceSettings: '高级设置',
|
|
4
4
|
agentDefaultMessage:
|
|
5
|
-
'你好,我是 **{{name}}**,你可以立即与我开始对话,也可以前往 [助手设置](/chat/{{id}}
|
|
5
|
+
'你好,我是 **{{name}}**,你可以立即与我开始对话,也可以前往 [助手设置](/chat/setting#session={{id}}) 完善我的信息。',
|
|
6
6
|
agentDefaultMessageWithSystemRole: '你好,我是 **{{name}}**,{{systemRole}},让我们开始对话吧!',
|
|
7
7
|
agentMaxToken: '会话最大长度',
|
|
8
8
|
agentModel: '模型',
|
|
@@ -24,7 +24,7 @@ const MobileHeader = memo(() => {
|
|
|
24
24
|
return (
|
|
25
25
|
<MobileNavBar
|
|
26
26
|
center={<MobileNavBarTitle desc={model} title={displayTitle} />}
|
|
27
|
-
onBackClick={() => Router.push(
|
|
27
|
+
onBackClick={() => Router.push({ hash: null, pathname: `/chat` })}
|
|
28
28
|
right={
|
|
29
29
|
<>
|
|
30
30
|
<ActionIcon icon={LayoutList} onClick={() => toggleConfig()} />
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui';
|
|
2
|
+
import Router from 'next/router';
|
|
2
3
|
import { ReactNode, memo } from 'react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
|
|
5
|
-
import { useSessionStore } from '@/store/session';
|
|
6
|
-
|
|
7
6
|
const Header = memo<{ children: ReactNode }>(({ children }) => {
|
|
8
7
|
const { t } = useTranslation('setting');
|
|
9
|
-
const onBack = useSessionStore((s) => s.switchBackToChat);
|
|
10
8
|
|
|
11
9
|
return (
|
|
12
10
|
<ChatHeader
|
|
13
11
|
left={<ChatHeaderTitle title={t('header.session')} />}
|
|
14
|
-
onBackClick={
|
|
12
|
+
onBackClick={() => Router.push({ hash: location.hash, pathname: `/chat` })}
|
|
15
13
|
right={children}
|
|
16
14
|
showBackButton
|
|
17
15
|
/>
|
|
@@ -9,7 +9,7 @@ const Header = memo<{ children: ReactNode }>(({ children }) => {
|
|
|
9
9
|
return (
|
|
10
10
|
<MobileNavBar
|
|
11
11
|
center={<MobileNavBarTitle title={t('header.session')} />}
|
|
12
|
-
onBackClick={() => Router.
|
|
12
|
+
onBackClick={() => Router.push({ hash: location.hash, pathname: `/chat/mobile` })}
|
|
13
13
|
right={children}
|
|
14
14
|
showBackButton
|
|
15
15
|
/>
|
|
@@ -1,53 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ChatHeader, Logo, SearchBar } from '@lobehub/ui';
|
|
2
2
|
import { createStyles } from 'antd-style';
|
|
3
|
-
import { MessageSquarePlus } from 'lucide-react';
|
|
4
3
|
import Link from 'next/link';
|
|
5
4
|
import { memo } from 'react';
|
|
6
5
|
import { useTranslation } from 'react-i18next';
|
|
7
|
-
import { Flexbox } from 'react-layout-kit';
|
|
8
|
-
|
|
9
|
-
import { useSessionStore } from '@/store/session';
|
|
10
6
|
|
|
11
7
|
export const useStyles = createStyles(({ css, token }) => ({
|
|
12
8
|
logo: css`
|
|
9
|
+
color: ${token.colorText};
|
|
13
10
|
fill: ${token.colorText};
|
|
14
11
|
`,
|
|
15
|
-
top: css`
|
|
16
|
-
position: sticky;
|
|
17
|
-
top: 0;
|
|
18
|
-
`,
|
|
19
12
|
}));
|
|
20
13
|
|
|
21
14
|
const Header = memo(() => {
|
|
22
15
|
const { styles } = useStyles();
|
|
23
16
|
const { t } = useTranslation('common');
|
|
24
|
-
const [keywords, createSession] = useSessionStore((s) => [s.searchKeywords, s.createSession]);
|
|
25
17
|
|
|
26
18
|
return (
|
|
27
|
-
<
|
|
28
|
-
|
|
19
|
+
<ChatHeader
|
|
20
|
+
left={
|
|
29
21
|
<Link href={'/'}>
|
|
30
|
-
<Logo className={styles.logo} size={36} type={'text'} />
|
|
22
|
+
<Logo className={styles.logo} extra={t('tab.market')} size={36} type={'text'} />
|
|
31
23
|
</Link>
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
size={{ fontSize: 24 }}
|
|
36
|
-
style={{ flex: 'none' }}
|
|
37
|
-
title={t('newAgent')}
|
|
38
|
-
/>
|
|
39
|
-
</Flexbox>
|
|
40
|
-
<SearchBar
|
|
41
|
-
allowClear
|
|
42
|
-
enableShortKey
|
|
43
|
-
onChange={(e) => useSessionStore.setState({ searchKeywords: e.target.value })}
|
|
44
|
-
placeholder={t('searchAgentPlaceholder')}
|
|
45
|
-
shortKey={'k'}
|
|
46
|
-
spotlight
|
|
47
|
-
type={'ghost'}
|
|
48
|
-
value={keywords}
|
|
49
|
-
/>
|
|
50
|
-
</Flexbox>
|
|
24
|
+
}
|
|
25
|
+
right={<SearchBar allowClear spotlight type={'ghost'} />}
|
|
26
|
+
/>
|
|
51
27
|
);
|
|
52
28
|
});
|
|
53
29
|
|
|
@@ -9,11 +9,9 @@ export const useGlobalHydrated = () => {
|
|
|
9
9
|
const [isInit, setInit] = useState(hasInited);
|
|
10
10
|
|
|
11
11
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
setInit(true);
|
|
16
|
-
}
|
|
12
|
+
useGlobalStore.persist.onFinishHydration(() => {
|
|
13
|
+
if (!isInit) setInit(true);
|
|
14
|
+
});
|
|
17
15
|
}, []);
|
|
18
16
|
|
|
19
17
|
return isInit;
|
|
@@ -9,10 +9,8 @@ import { GlobalStore, useGlobalStore } from '../store';
|
|
|
9
9
|
*/
|
|
10
10
|
export const useOnFinishHydrationGlobal = (fn: (state: GlobalStore) => void, deps: any[] = []) => {
|
|
11
11
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
fn(useGlobalStore.getState());
|
|
12
|
+
useGlobalStore.persist.onFinishHydration(() => {
|
|
13
|
+
fn(useGlobalStore.getState());
|
|
14
|
+
});
|
|
17
15
|
}, deps);
|
|
18
16
|
};
|
|
@@ -1,28 +1,40 @@
|
|
|
1
|
-
import { isEmpty } from 'lodash-es';
|
|
2
1
|
import { PersistStorage } from 'zustand/middleware';
|
|
3
2
|
import { StorageValue } from 'zustand/middleware/persist';
|
|
4
3
|
|
|
4
|
+
import { createIndexedDB } from './indexedDB';
|
|
5
5
|
import { createKeyMapper } from './keyMapper';
|
|
6
|
+
import { createLocalStorage } from './localStorage';
|
|
6
7
|
import { HyperStorageOptions } from './type';
|
|
7
|
-
import {
|
|
8
|
+
import { creatUrlStorage } from './urlStorage';
|
|
8
9
|
|
|
9
10
|
export const createHyperStorage = <T extends object>(
|
|
10
11
|
options: HyperStorageOptions,
|
|
11
12
|
): PersistStorage<T> => {
|
|
12
13
|
const hasUrl = !!options.url;
|
|
13
|
-
|
|
14
|
-
const
|
|
14
|
+
const useIndexedDB = options.localStorage?.mode === 'indexedDB';
|
|
15
|
+
const dbName = options.localStorage?.dbName || 'indexedDB';
|
|
15
16
|
|
|
16
17
|
const { mapStateKeyToStorageKey, getStateKeyFromStorageKey } = createKeyMapper(options);
|
|
17
18
|
|
|
19
|
+
const indexedDB = createIndexedDB(dbName);
|
|
20
|
+
const localStorage = createLocalStorage();
|
|
21
|
+
const urlStorage = creatUrlStorage(options.url?.mode);
|
|
18
22
|
return {
|
|
19
|
-
getItem: (name): StorageValue<T
|
|
23
|
+
getItem: async (name): Promise<StorageValue<T>> => {
|
|
20
24
|
const state: any = {};
|
|
21
25
|
let version: number | undefined;
|
|
26
|
+
|
|
22
27
|
// ============== 处理 Local Storage ============== //
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
let localState: StorageValue<T> | undefined;
|
|
29
|
+
|
|
30
|
+
// 如果使用 indexedDB,优先从 indexedDB 中获取
|
|
31
|
+
if (useIndexedDB) {
|
|
32
|
+
localState = await indexedDB.getItem(name);
|
|
33
|
+
}
|
|
34
|
+
// 如果 indexedDB 中不存在,则再试试 localStorage
|
|
35
|
+
if (!localState) localState = localStorage.getItem(name);
|
|
36
|
+
|
|
37
|
+
if (localState) {
|
|
26
38
|
version = localState.version;
|
|
27
39
|
for (const [k, v] of Object.entries(localState.state)) {
|
|
28
40
|
const key = getStateKeyFromStorageKey(k, 'localStorage');
|
|
@@ -33,31 +45,39 @@ export const createHyperStorage = <T extends object>(
|
|
|
33
45
|
// ============== 处理 URL Storage ============== //
|
|
34
46
|
// 不从 URL 中获取 version,由于 url 状态是临时态 不作为版本控制的依据
|
|
35
47
|
if (hasUrl) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
const urlState = urlStorage.getItem();
|
|
49
|
+
|
|
50
|
+
if (urlState) {
|
|
51
|
+
for (const [k, v] of Object.entries(urlState.state)) {
|
|
52
|
+
const key = getStateKeyFromStorageKey(k, 'url');
|
|
53
|
+
// 当存在 UrlSelector 逻辑,且 key 有效时,才将状态加入终态 state
|
|
54
|
+
if (hasUrl && key) {
|
|
55
|
+
state[key] = v;
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
58
|
}
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
return { state, version };
|
|
48
62
|
},
|
|
49
|
-
removeItem: (key)
|
|
63
|
+
removeItem: async (key) => {
|
|
64
|
+
// ============== 处理 Local Storage ============== //
|
|
65
|
+
|
|
66
|
+
if (useIndexedDB) {
|
|
67
|
+
await indexedDB.removeItem(key);
|
|
68
|
+
} else {
|
|
69
|
+
localStorage.removeItem(key);
|
|
70
|
+
}
|
|
71
|
+
|
|
50
72
|
// ============== 处理 URL Storage ============== //
|
|
51
73
|
if (hasUrl) {
|
|
52
|
-
const searchParameters = new URLSearchParams(getUrlSearch());
|
|
53
|
-
|
|
54
74
|
const storageKey = getStateKeyFromStorageKey(key, 'url');
|
|
55
|
-
if (storageKey) searchParameters.delete(storageKey);
|
|
56
75
|
|
|
57
|
-
|
|
76
|
+
urlStorage.removeItem(storageKey);
|
|
58
77
|
}
|
|
59
78
|
},
|
|
60
|
-
|
|
79
|
+
|
|
80
|
+
setItem: async (name, newValue) => {
|
|
61
81
|
// ============== 处理 Local Storage ============== //
|
|
62
82
|
const localState: Record<string, any> = {};
|
|
63
83
|
for (const [k, v] of Object.entries(newValue.state)) {
|
|
@@ -65,30 +85,24 @@ export const createHyperStorage = <T extends object>(
|
|
|
65
85
|
if (localKey) localState[localKey] = v;
|
|
66
86
|
}
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
if (useIndexedDB) {
|
|
89
|
+
await indexedDB.setItem(name, localState, newValue.version);
|
|
90
|
+
} else {
|
|
91
|
+
localStorage.setItem(name, localState, newValue.version);
|
|
92
|
+
}
|
|
69
93
|
|
|
70
94
|
// ============== 处理 URL Storage ============== //
|
|
71
95
|
if (hasUrl) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (typeof v === 'string') {
|
|
85
|
-
searchParameters.set(urlKey, v);
|
|
86
|
-
} else {
|
|
87
|
-
searchParameters.set(urlKey, JSON.stringify(v));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
setUrlSearch(searchParameters);
|
|
96
|
+
// 转换 key 为目标 key
|
|
97
|
+
const finalEntries = Object.entries(newValue.state)
|
|
98
|
+
.map(([k, v]) => {
|
|
99
|
+
const urlKey = mapStateKeyToStorageKey(k, 'url');
|
|
100
|
+
if (!urlKey) return undefined;
|
|
101
|
+
return [urlKey, v];
|
|
102
|
+
})
|
|
103
|
+
.filter(Boolean) as [string, any][];
|
|
104
|
+
|
|
105
|
+
urlStorage.setItem(name, Object.fromEntries(finalEntries));
|
|
92
106
|
}
|
|
93
107
|
},
|
|
94
108
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createStore, delMany, getMany, setMany } from 'idb-keyval';
|
|
2
|
+
import { StorageValue } from 'zustand/middleware/persist';
|
|
3
|
+
|
|
4
|
+
export const createIndexedDB = <State extends any>(dbName: string = 'indexedDB') => ({
|
|
5
|
+
getItem: async <T extends State>(name: string): Promise<StorageValue<T> | undefined> => {
|
|
6
|
+
const [version, state] = await getMany(['version', 'state'], createStore(dbName, name));
|
|
7
|
+
|
|
8
|
+
if (!state) return undefined;
|
|
9
|
+
|
|
10
|
+
return { state, version };
|
|
11
|
+
},
|
|
12
|
+
removeItem: async (name: string) => {
|
|
13
|
+
await delMany(['version', 'state'], createStore(dbName, name));
|
|
14
|
+
},
|
|
15
|
+
setItem: async (name: string, state: any, version: number | undefined) => {
|
|
16
|
+
const store = createStore(dbName, name);
|
|
17
|
+
|
|
18
|
+
await setMany(
|
|
19
|
+
[
|
|
20
|
+
['version', version],
|
|
21
|
+
['state', state],
|
|
22
|
+
],
|
|
23
|
+
store,
|
|
24
|
+
);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StorageValue } from 'zustand/middleware/persist';
|
|
2
|
+
|
|
3
|
+
export const createLocalStorage = <State extends any>() => ({
|
|
4
|
+
getItem: <T extends State>(name: string): StorageValue<T> | undefined => {
|
|
5
|
+
const string = localStorage.getItem(name);
|
|
6
|
+
|
|
7
|
+
if (string) return JSON.parse(string) as StorageValue<T>;
|
|
8
|
+
|
|
9
|
+
return undefined;
|
|
10
|
+
},
|
|
11
|
+
removeItem: (name: string) => {
|
|
12
|
+
localStorage.removeItem(name);
|
|
13
|
+
},
|
|
14
|
+
setItem: <T extends State>(name: string, state: T, version: number | undefined) => {
|
|
15
|
+
localStorage.setItem(name, JSON.stringify({ state, version }));
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -2,6 +2,11 @@ export type StorageSelector = string | Record<string, string>;
|
|
|
2
2
|
|
|
3
3
|
export interface HyperStorageOptions {
|
|
4
4
|
localStorage?: {
|
|
5
|
+
dbName?: string;
|
|
6
|
+
/**
|
|
7
|
+
* @default 'localStorage'
|
|
8
|
+
*/
|
|
9
|
+
mode?: 'indexedDB' | 'localStorage';
|
|
5
10
|
selectors: StorageSelector[];
|
|
6
11
|
};
|
|
7
12
|
url?: {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isEmpty } from 'lodash-es';
|
|
2
|
+
import { StorageValue } from 'zustand/middleware/persist';
|
|
3
|
+
|
|
4
|
+
interface UrlSearchHelper {
|
|
5
|
+
getUrlSearch: () => string;
|
|
6
|
+
setUrlSearch: (params: URLSearchParams) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const createUrlSearch = (mode: 'search' | 'hash' = 'hash'): UrlSearchHelper => {
|
|
10
|
+
if (mode === 'hash')
|
|
11
|
+
return {
|
|
12
|
+
getUrlSearch: () => location.hash.slice(1),
|
|
13
|
+
setUrlSearch: (params: URLSearchParams) => (location.hash = params.toString()),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
getUrlSearch: () => location.search.slice(1),
|
|
18
|
+
setUrlSearch: (params: URLSearchParams) => (location.search = params.toString()),
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const creatUrlStorage = <State extends object>(mode: 'hash' | 'search' = 'hash') => {
|
|
23
|
+
const { setUrlSearch, getUrlSearch } = createUrlSearch(mode);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
getItem: <T extends State>(): StorageValue<T> | undefined => {
|
|
27
|
+
const searchParameters = new URLSearchParams(getUrlSearch());
|
|
28
|
+
|
|
29
|
+
if (searchParameters.size === 0) return undefined;
|
|
30
|
+
|
|
31
|
+
const state = Object.fromEntries(searchParameters.entries()) as T;
|
|
32
|
+
|
|
33
|
+
return { state };
|
|
34
|
+
},
|
|
35
|
+
removeItem: (key?: string) => {
|
|
36
|
+
const searchParameters = new URLSearchParams(getUrlSearch());
|
|
37
|
+
if (key) searchParameters.delete(key);
|
|
38
|
+
|
|
39
|
+
setUrlSearch(searchParameters);
|
|
40
|
+
},
|
|
41
|
+
setItem: <T extends State>(name: string, state: T) => {
|
|
42
|
+
const searchParameters = new URLSearchParams(getUrlSearch());
|
|
43
|
+
|
|
44
|
+
for (const [urlKey, v] of Object.entries(state)) {
|
|
45
|
+
if (isEmpty(v)) {
|
|
46
|
+
searchParameters.delete(urlKey);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof v === 'string') {
|
|
51
|
+
searchParameters.set(urlKey, v);
|
|
52
|
+
} else {
|
|
53
|
+
searchParameters.set(urlKey, JSON.stringify(v));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setUrlSearch(searchParameters);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
@@ -12,10 +12,9 @@ export const useOnFinishHydrationSession = (
|
|
|
12
12
|
deps: any[] = [],
|
|
13
13
|
) => {
|
|
14
14
|
useEffect(() => {
|
|
15
|
-
const hasRehydrated = useSessionStore.persist.hasHydrated();
|
|
16
15
|
// 只有当水合完毕后再开始做操作
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
useSessionStore.persist.onFinishHydration(() => {
|
|
17
|
+
fn(useSessionStore.getState());
|
|
18
|
+
});
|
|
20
19
|
}, deps);
|
|
21
20
|
};
|
|
@@ -5,14 +5,13 @@ import { useSessionStore } from '../store';
|
|
|
5
5
|
export const useSessionHydrated = () => {
|
|
6
6
|
// 根据 sessions 是否有值来判断是否已经初始化
|
|
7
7
|
const hasInited = !!Object.values(useSessionStore.getState().sessions).length;
|
|
8
|
+
|
|
8
9
|
const [isInit, setInit] = useState(hasInited);
|
|
9
10
|
|
|
10
11
|
useEffect(() => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
setInit(true);
|
|
15
|
-
}
|
|
12
|
+
useSessionStore.persist.onFinishHydration(() => {
|
|
13
|
+
if (!isInit) setInit(true);
|
|
14
|
+
});
|
|
16
15
|
}, []);
|
|
17
16
|
|
|
18
17
|
return isInit;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
interface UrlSearchHelper {
|
|
2
|
-
getUrlSearch: () => string;
|
|
3
|
-
setUrlSearch: (params: URLSearchParams) => void;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export const createUrlSearch = (mode: 'search' | 'hash' = 'hash'): UrlSearchHelper => {
|
|
7
|
-
if (mode === 'hash')
|
|
8
|
-
return {
|
|
9
|
-
getUrlSearch: () => location.hash.slice(1),
|
|
10
|
-
setUrlSearch: (params: URLSearchParams) => (location.hash = params.toString()),
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
getUrlSearch: () => location.search.slice(1),
|
|
15
|
-
setUrlSearch: (params: URLSearchParams) => (location.search = params.toString()),
|
|
16
|
-
};
|
|
17
|
-
};
|