@lobehub/chat 0.101.0 → 0.101.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.
- package/CHANGELOG.md +25 -0
- package/package.json +1 -1
- package/src/app/market/(desktop)/features/AgentDetail.tsx +14 -9
- package/src/app/market/(desktop)/index.tsx +2 -3
- package/src/app/market/(mobile)/index.tsx +2 -3
- package/src/app/market/features/AgentCard/AgentCardItem.tsx +4 -1
- package/src/app/market/features/AgentCard/index.tsx +20 -31
- package/src/app/market/features/AgentDetailContent/AgentInfo/Header.tsx +0 -1
- package/src/app/market/features/AgentDetailContent/AgentInfo/index.tsx +2 -3
- package/src/app/market/features/AgentSearchBar/index.tsx +9 -4
- package/src/app/market/features/TagList/Inner.tsx +28 -0
- package/src/app/market/features/TagList/index.tsx +12 -23
- package/src/store/market/action.ts +9 -1
- package/src/store/market/initialState.ts +2 -0
- package/src/store/market/selectors.ts +10 -8
- package/src/types/market.ts +1 -0
- package/src/utils/findDuplicates.ts +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 0.101.1](https://github.com/lobehub/lobe-chat/compare/v0.101.0...v0.101.1)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2023-11-14**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Fix market search (fix.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Fix market search (fix, closes [#437](https://github.com/lobehub/lobe-chat/issues/437) ([178b742](https://github.com/lobehub/lobe-chat/commit/178b742))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 0.101.0](https://github.com/lobehub/lobe-chat/compare/v0.100.5...v0.101.0)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2023-11-14**</sup>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/chat",
|
|
3
|
-
"version": "0.101.
|
|
3
|
+
"version": "0.101.1",
|
|
4
4
|
"description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"chatbot",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DraggablePanel, DraggablePanelBody, DraggablePanelContainer } from '@lobehub/ui';
|
|
2
2
|
import { createStyles } from 'antd-style';
|
|
3
|
-
import { memo, useState } from 'react';
|
|
3
|
+
import { memo, useCallback, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import SafeSpacing from '@/components/SafeSpacing';
|
|
6
6
|
import { MARKET_SIDEBAR_WIDTH } from '@/const/layoutTokens';
|
|
@@ -31,6 +31,18 @@ const SideBar = memo(() => {
|
|
|
31
31
|
s.activateAgent,
|
|
32
32
|
]);
|
|
33
33
|
|
|
34
|
+
const handleExpandChange = useCallback(
|
|
35
|
+
(show: boolean) => {
|
|
36
|
+
if (!show) {
|
|
37
|
+
setTempId(useMarketStore.getState().currentIdentifier);
|
|
38
|
+
deactivateAgent();
|
|
39
|
+
} else if (tempId) {
|
|
40
|
+
activateAgent(tempId);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
[deactivateAgent, activateAgent, tempId],
|
|
44
|
+
);
|
|
45
|
+
|
|
34
46
|
return (
|
|
35
47
|
<DraggablePanel
|
|
36
48
|
className={styles.drawer}
|
|
@@ -40,14 +52,7 @@ const SideBar = memo(() => {
|
|
|
40
52
|
expand={showAgentSidebar}
|
|
41
53
|
minWidth={MARKET_SIDEBAR_WIDTH}
|
|
42
54
|
mode={'fixed'}
|
|
43
|
-
onExpandChange={
|
|
44
|
-
if (!show) {
|
|
45
|
-
setTempId(useMarketStore.getState().currentIdentifier);
|
|
46
|
-
deactivateAgent();
|
|
47
|
-
} else if (tempId) {
|
|
48
|
-
activateAgent(tempId);
|
|
49
|
-
}
|
|
50
|
-
}}
|
|
55
|
+
onExpandChange={handleExpandChange}
|
|
51
56
|
placement={'right'}
|
|
52
57
|
>
|
|
53
58
|
<DraggablePanelContainer
|
|
@@ -6,18 +6,17 @@ import { FC, memo } from 'react';
|
|
|
6
6
|
|
|
7
7
|
import AgentCard from '@/app/market/features/AgentCard';
|
|
8
8
|
import ResponsiveIndex from '@/components/ResponsiveIndex';
|
|
9
|
-
import { AgentsMarketIndexItem } from '@/types/market';
|
|
10
9
|
|
|
11
10
|
import Index from '../index';
|
|
12
11
|
import Layout from './layout.desktop';
|
|
13
12
|
|
|
14
13
|
const Mobile: FC = dynamic(() => import('../(mobile)'), { ssr: false }) as FC;
|
|
15
14
|
|
|
16
|
-
export default memo
|
|
15
|
+
export default memo(() => (
|
|
17
16
|
<ResponsiveIndex Mobile={Mobile}>
|
|
18
17
|
<Layout>
|
|
19
18
|
<Index />
|
|
20
|
-
<AgentCard CardRender={SpotlightCard}
|
|
19
|
+
<AgentCard CardRender={SpotlightCard} />
|
|
21
20
|
</Layout>
|
|
22
21
|
</ResponsiveIndex>
|
|
23
22
|
));
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
import { memo } from 'react';
|
|
4
4
|
|
|
5
5
|
import AgentCard from '@/app/market/features/AgentCard';
|
|
6
|
-
import { AgentsMarketIndexItem } from '@/types/market';
|
|
7
6
|
|
|
8
7
|
import Index from '../index';
|
|
9
8
|
import CardRender from './features/AgentCard';
|
|
10
9
|
import Layout from './layout.mobile';
|
|
11
10
|
|
|
12
|
-
export default memo
|
|
11
|
+
export default memo(() => (
|
|
13
12
|
<Layout>
|
|
14
13
|
<Index />
|
|
15
|
-
<AgentCard CardRender={CardRender}
|
|
14
|
+
<AgentCard CardRender={CardRender} mobile />
|
|
16
15
|
</Layout>
|
|
17
16
|
));
|
|
@@ -13,13 +13,16 @@ import AgentCardBanner from './AgentCardBanner';
|
|
|
13
13
|
import { useStyles } from './style';
|
|
14
14
|
|
|
15
15
|
const { Paragraph } = Typography;
|
|
16
|
+
|
|
16
17
|
const AgentCardItem = memo<AgentsMarketIndexItem>(({ meta, identifier }) => {
|
|
17
18
|
const ref = useRef(null);
|
|
18
19
|
const isHovering = useHover(ref);
|
|
19
20
|
const onAgentCardClick = useMarketStore((s) => s.activateAgent);
|
|
20
|
-
const { avatar, title, description, tags, backgroundColor } = meta;
|
|
21
21
|
const { styles, theme } = useStyles();
|
|
22
22
|
const { isDarkMode } = useThemeMode();
|
|
23
|
+
|
|
24
|
+
const { avatar, title, description, tags, backgroundColor } = meta;
|
|
25
|
+
|
|
23
26
|
return (
|
|
24
27
|
<Flexbox className={styles.container} onClick={() => onAgentCardClick(identifier)}>
|
|
25
28
|
<AgentCardBanner meta={meta} style={{ opacity: isDarkMode ? 0.9 : 0.4 }} />
|
|
@@ -1,71 +1,60 @@
|
|
|
1
1
|
import { SpotlightCardProps } from '@lobehub/ui';
|
|
2
|
-
import
|
|
2
|
+
import isEqual from 'fast-deep-equal';
|
|
3
|
+
import { FC, memo, useCallback } from 'react';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import { Flexbox } from 'react-layout-kit';
|
|
5
6
|
import LazyLoad from 'react-lazy-load';
|
|
6
7
|
|
|
7
8
|
import { agentMarketSelectors, useMarketStore } from '@/store/market';
|
|
8
|
-
import { AgentsMarketIndexItem } from '@/types/market';
|
|
9
9
|
|
|
10
10
|
import TagList from '../TagList';
|
|
11
11
|
import AgentCardItem from './AgentCardItem';
|
|
12
12
|
import Loading from './Loading';
|
|
13
13
|
import { useStyles } from './style';
|
|
14
14
|
|
|
15
|
-
const gridRender: SpotlightCardProps['renderItem'] = (item) => {
|
|
16
|
-
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
17
|
-
const { styles } = useStyles();
|
|
18
|
-
return (
|
|
19
|
-
<LazyLoad className={styles.lazy}>
|
|
20
|
-
<AgentCardItem {...item} />
|
|
21
|
-
</LazyLoad>
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
15
|
export interface AgentCardProps {
|
|
26
16
|
CardRender: FC<SpotlightCardProps>;
|
|
27
|
-
defaultAgents?: AgentsMarketIndexItem[];
|
|
28
17
|
mobile?: boolean;
|
|
29
18
|
}
|
|
30
19
|
|
|
31
|
-
const AgentCard = memo<AgentCardProps>(({
|
|
20
|
+
const AgentCard = memo<AgentCardProps>(({ CardRender, mobile }) => {
|
|
32
21
|
const { t } = useTranslation('market');
|
|
33
|
-
|
|
34
|
-
const { styles } = useStyles();
|
|
35
|
-
|
|
36
22
|
const [useFetchAgentList, keywords] = useMarketStore((s) => [
|
|
37
23
|
s.useFetchAgentList,
|
|
38
24
|
s.searchKeywords,
|
|
39
25
|
]);
|
|
40
|
-
useFetchAgentList();
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const agentList = clientAgentList.length === 0 && defaultAgents ? defaultAgents : clientAgentList;
|
|
26
|
+
const { isLoading } = useFetchAgentList();
|
|
27
|
+
const agentList = useMarketStore(agentMarketSelectors.getAgentList, isEqual);
|
|
28
|
+
const { styles } = useStyles();
|
|
44
29
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
const GridRender: SpotlightCardProps['renderItem'] = useCallback(
|
|
31
|
+
(props: any) => (
|
|
32
|
+
<LazyLoad className={styles.lazy}>
|
|
33
|
+
<AgentCardItem {...props} />
|
|
34
|
+
</LazyLoad>
|
|
35
|
+
),
|
|
36
|
+
[styles.lazy],
|
|
37
|
+
);
|
|
49
38
|
|
|
50
|
-
if (
|
|
39
|
+
if (isLoading) return <Loading />;
|
|
51
40
|
|
|
52
41
|
return (
|
|
53
42
|
<Flexbox gap={mobile ? 16 : 24}>
|
|
54
43
|
<TagList />
|
|
55
44
|
{keywords ? (
|
|
56
45
|
<CardRender
|
|
57
|
-
items={
|
|
58
|
-
renderItem={
|
|
46
|
+
items={agentList}
|
|
47
|
+
renderItem={GridRender}
|
|
59
48
|
spotlight={mobile ? undefined : false}
|
|
60
49
|
/>
|
|
61
50
|
) : (
|
|
62
51
|
<>
|
|
63
52
|
<div className={styles.subTitle}>{t('title.recentSubmits')}</div>
|
|
64
|
-
<CardRender items={
|
|
53
|
+
<CardRender items={agentList.slice(0, 3)} renderItem={GridRender} />
|
|
65
54
|
<div className={styles.subTitle}>{t('title.allAgents')}</div>
|
|
66
55
|
<CardRender
|
|
67
|
-
items={
|
|
68
|
-
renderItem={
|
|
56
|
+
items={agentList.slice(3)}
|
|
57
|
+
renderItem={GridRender}
|
|
69
58
|
spotlight={mobile ? undefined : false}
|
|
70
59
|
/>
|
|
71
60
|
</>
|
|
@@ -17,7 +17,6 @@ const { Link } = Typography;
|
|
|
17
17
|
const Header = memo(() => {
|
|
18
18
|
const { t } = useTranslation('market');
|
|
19
19
|
const { styles, theme } = useStyles();
|
|
20
|
-
|
|
21
20
|
const createSession = useSessionStore((s) => s.createSession);
|
|
22
21
|
const switchSideBar = useGlobalStore((s) => s.switchSideBar);
|
|
23
22
|
const agentItem = useMarketStore(agentMarketSelectors.currentAgentItem);
|
|
@@ -22,11 +22,10 @@ const AgentModalInner = memo(() => {
|
|
|
22
22
|
s.useFetchAgent,
|
|
23
23
|
s.currentIdentifier,
|
|
24
24
|
]);
|
|
25
|
-
const { styles } = useStyles();
|
|
26
|
-
|
|
27
|
-
const { data, isLoading } = useFetchAgent(currentIdentifier);
|
|
28
25
|
const { t } = useTranslation('market');
|
|
29
26
|
const [tab, setTab] = useState<string>(InfoTabs.prompt);
|
|
27
|
+
const { data, isLoading } = useFetchAgent(currentIdentifier);
|
|
28
|
+
const { styles } = useStyles();
|
|
30
29
|
|
|
31
30
|
if (isLoading || !data?.meta) return <Loading />;
|
|
32
31
|
|
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
import { SearchBar } from '@lobehub/ui';
|
|
2
2
|
import { useResponsive } from 'antd-style';
|
|
3
|
-
import { memo, useState } from 'react';
|
|
3
|
+
import { memo, useCallback, useState } from 'react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
|
|
6
6
|
import { useMarketStore } from '@/store/market';
|
|
7
7
|
|
|
8
8
|
const AgentSearchBar = memo(() => {
|
|
9
9
|
const { t } = useTranslation('market');
|
|
10
|
-
const keywords = useMarketStore((s) => s.searchKeywords);
|
|
10
|
+
const [keywords, setKeywords] = useMarketStore((s) => [s.searchKeywords, s.setSearchKeywords]);
|
|
11
11
|
const [value, setValue] = useState(keywords);
|
|
12
12
|
const { mobile } = useResponsive();
|
|
13
|
+
|
|
14
|
+
const handleSearch = useCallback(() => {
|
|
15
|
+
setKeywords(value);
|
|
16
|
+
}, [value, setKeywords]);
|
|
17
|
+
|
|
13
18
|
return (
|
|
14
19
|
<SearchBar
|
|
15
20
|
allowClear
|
|
16
21
|
enableShortKey={!mobile}
|
|
17
22
|
onChange={(e) => setValue(e.target.value)}
|
|
18
|
-
onPressEnter={
|
|
19
|
-
onSubmit={
|
|
23
|
+
onPressEnter={handleSearch}
|
|
24
|
+
onSubmit={handleSearch}
|
|
20
25
|
placeholder={t('search.placeholder')}
|
|
21
26
|
shortKey={'k'}
|
|
22
27
|
spotlight={!mobile}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Button } from 'antd';
|
|
2
|
+
import isEqual from 'fast-deep-equal';
|
|
3
|
+
import { startCase } from 'lodash-es';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { agentMarketSelectors, useMarketStore } from '@/store/market';
|
|
7
|
+
|
|
8
|
+
const Inner = memo(() => {
|
|
9
|
+
const agentTagList = useMarketStore(agentMarketSelectors.getAgentTagList, isEqual);
|
|
10
|
+
const [keywords, setSearchKeywords] = useMarketStore((s) => [
|
|
11
|
+
s.searchKeywords,
|
|
12
|
+
s.setSearchKeywords,
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
return agentTagList.map((item) => (
|
|
16
|
+
<Button
|
|
17
|
+
key={item}
|
|
18
|
+
onClick={() => setSearchKeywords(item)}
|
|
19
|
+
shape={'round'}
|
|
20
|
+
size={'small'}
|
|
21
|
+
type={keywords === item ? 'primary' : 'default'}
|
|
22
|
+
>
|
|
23
|
+
{startCase(item)}
|
|
24
|
+
</Button>
|
|
25
|
+
));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export default Inner;
|
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import { startCase } from 'lodash-es';
|
|
4
|
-
import { memo } from 'react';
|
|
1
|
+
import { Skeleton } from 'antd';
|
|
2
|
+
import { Suspense, memo } from 'react';
|
|
5
3
|
import { Flexbox } from 'react-layout-kit';
|
|
6
4
|
|
|
7
|
-
import
|
|
5
|
+
import Inner from '@/app/market/features/TagList/Inner';
|
|
8
6
|
|
|
9
7
|
const TagList = memo(() => {
|
|
10
|
-
const agentTagList = useMarketStore(agentMarketSelectors.getAgentTagList, isEqual);
|
|
11
|
-
const keywords = useMarketStore((s) => s.searchKeywords);
|
|
12
|
-
|
|
13
8
|
return (
|
|
14
9
|
<Flexbox gap={6} horizontal style={{ flexWrap: 'wrap' }}>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{startCase(item)}
|
|
25
|
-
</Button>
|
|
26
|
-
))
|
|
27
|
-
: Array.from({ length: 5 })
|
|
28
|
-
.fill('')
|
|
29
|
-
.map((_, index) => <Skeleton.Button key={index} shape={'round'} size={'small'} />)}
|
|
10
|
+
<Suspense
|
|
11
|
+
fallback={Array.from({ length: 5 })
|
|
12
|
+
.fill('')
|
|
13
|
+
.map((_, index) => (
|
|
14
|
+
<Skeleton.Button key={index} shape={'round'} size={'small'} />
|
|
15
|
+
))}
|
|
16
|
+
>
|
|
17
|
+
<Inner />
|
|
18
|
+
</Suspense>
|
|
30
19
|
</Flexbox>
|
|
31
20
|
);
|
|
32
21
|
});
|
|
@@ -12,6 +12,7 @@ import type { Store } from './store';
|
|
|
12
12
|
export interface StoreAction {
|
|
13
13
|
activateAgent: (identifier: string) => void;
|
|
14
14
|
deactivateAgent: () => void;
|
|
15
|
+
setSearchKeywords: (keywords: string) => void;
|
|
15
16
|
updateAgentMap: (key: string, value: AgentsMarketItem) => void;
|
|
16
17
|
useFetchAgent: (identifier: string) => SWRResponse<AgentsMarketItem>;
|
|
17
18
|
useFetchAgentList: () => SWRResponse<LobeChatAgentsMarketIndex>;
|
|
@@ -29,6 +30,9 @@ export const createMarketAction: StateCreator<
|
|
|
29
30
|
deactivateAgent: () => {
|
|
30
31
|
set({ currentIdentifier: undefined }, false, 'deactivateAgent');
|
|
31
32
|
},
|
|
33
|
+
setSearchKeywords: (keywords) => {
|
|
34
|
+
set({ searchKeywords: keywords });
|
|
35
|
+
},
|
|
32
36
|
updateAgentMap: (key, value) => {
|
|
33
37
|
const { agentMap } = get();
|
|
34
38
|
|
|
@@ -56,7 +60,11 @@ export const createMarketAction: StateCreator<
|
|
|
56
60
|
useFetchAgentList: () =>
|
|
57
61
|
useSWR<LobeChatAgentsMarketIndex>(getCurrentLanguage(), getAgentList, {
|
|
58
62
|
onSuccess: (agentMarketIndex) => {
|
|
59
|
-
set(
|
|
63
|
+
set(
|
|
64
|
+
{ agentList: agentMarketIndex.agents, tagList: agentMarketIndex.tags },
|
|
65
|
+
false,
|
|
66
|
+
'useFetchAgentList',
|
|
67
|
+
);
|
|
60
68
|
},
|
|
61
69
|
}),
|
|
62
70
|
});
|
|
@@ -7,6 +7,7 @@ export interface StoreState {
|
|
|
7
7
|
agentMap: MarketAgentMap;
|
|
8
8
|
currentIdentifier: string;
|
|
9
9
|
searchKeywords: string;
|
|
10
|
+
tagList: string[];
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export const initialState: StoreState = {
|
|
@@ -14,4 +15,5 @@ export const initialState: StoreState = {
|
|
|
14
15
|
agentMap: {},
|
|
15
16
|
currentIdentifier: '',
|
|
16
17
|
searchKeywords: '',
|
|
18
|
+
tagList: [],
|
|
17
19
|
};
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import { flatten } from 'lodash-es';
|
|
2
|
-
|
|
3
1
|
import { DEFAULT_AGENTS_MARKET_ITEM } from '@/const/market';
|
|
4
2
|
import { AgentsMarketItem } from '@/types/market';
|
|
5
|
-
import { findDuplicates } from '@/utils/findDuplicates';
|
|
6
3
|
|
|
7
4
|
import type { Store } from './store';
|
|
8
5
|
|
|
9
|
-
const getAgentList = (s: Store) =>
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
const getAgentList = (s: Store) => {
|
|
7
|
+
const { searchKeywords, agentList } = s;
|
|
8
|
+
if (!searchKeywords) return agentList;
|
|
9
|
+
return agentList.filter(({ meta }) => {
|
|
10
|
+
const checkMeta: string = [meta.tags, meta.title, meta.description, meta.avatar]
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
.join('');
|
|
13
|
+
return checkMeta.toLowerCase().includes(searchKeywords.toLowerCase());
|
|
14
|
+
});
|
|
14
15
|
};
|
|
16
|
+
const getAgentTagList = (s: Store) => s.tagList;
|
|
15
17
|
|
|
16
18
|
const getAgentItemById = (d: string) => (s: Store) => s.agentMap[d];
|
|
17
19
|
|
package/src/types/market.ts
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export const findDuplicates = (arr: string[]): string[] => {
|
|
2
|
-
const duplicates: { [key: string]: number } = {};
|
|
3
|
-
|
|
4
|
-
// 统计每个项目出现的次数
|
|
5
|
-
for (const item of arr) {
|
|
6
|
-
if (duplicates[item]) {
|
|
7
|
-
duplicates[item]++;
|
|
8
|
-
} else {
|
|
9
|
-
duplicates[item] = 1;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// 挑出重复出现 3 次以上的项目
|
|
14
|
-
const COUNT = 3;
|
|
15
|
-
|
|
16
|
-
const result = Object.keys(duplicates).filter((item) => duplicates[item] >= COUNT);
|
|
17
|
-
|
|
18
|
-
// 按重复次数从多到少排序
|
|
19
|
-
result.sort((a, b) => duplicates[b] - duplicates[a]);
|
|
20
|
-
|
|
21
|
-
return result;
|
|
22
|
-
};
|