feat: improve PageEditor header UX with DropdownMenu and i18n support (#11462)

*  feat: improve PageEditor header UX with DropdownMenu and i18n support

- Migrate Header from Dropdown to DropdownMenu component with checkbox support
- Add i18n for Ask Copilot item using common cmdk.askLobeAI key
- Replace BotIcon with Avatar using DEFAULT_INBOX_AVATAR
- Add hideWhenExpanded prop to ToggleRightPanelButton
- Conditionally show page info section only when lastUpdatedTime exists

* 🔧 chore: update @lobehub/ui dependency to version 4.18.0 in package.json

* feat: unify proxy setting style

Signed-off-by: Innei <tukon479@gmail.com>

* fix: test

Signed-off-by: Innei <tukon479@gmail.com>

* fix: test

Signed-off-by: Innei <tukon479@gmail.com>

---------

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei
2026-01-13 18:25:28 +08:00
committed by GitHub
parent 3503375529
commit ae499c9ab9
11 changed files with 292 additions and 335 deletions

View File

@@ -206,7 +206,7 @@
"@lobehub/icons": "^4.0.2",
"@lobehub/market-sdk": "0.28.1",
"@lobehub/tts": "^4.0.2",
"@lobehub/ui": "^4.11.6",
"@lobehub/ui": "^4.18.0",
"@modelcontextprotocol/sdk": "^1.25.1",
"@neondatabase/serverless": "^1.0.2",
"@next/third-parties": "^16.1.1",
@@ -454,4 +454,4 @@
"access": "public",
"registry": "https://registry.npmjs.org"
}
}
}

View File

@@ -3,7 +3,7 @@
import { Center, Empty, Markdown } from '@lobehub/ui';
import { FileText } from 'lucide-react';
import Link from 'next/link';
import { type ReactNode, memo } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { H1, H2, H3, H4, H5 } from './Toc/Heading';
@@ -26,7 +26,7 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
<Markdown
allowHtml
components={{
a: ({ href, ...rest }: { children: ReactNode; href: string }) => {
a: ({ href, ...rest }) => {
if (href && href.startsWith('http'))
return <Link {...rest} href={href} target={'_blank'} />;
return rest?.children;
@@ -36,12 +36,14 @@ const MarkdownRender = memo<{ children?: string }>(({ children }) => {
h3: H3,
h4: H4,
h5: H5,
img: ({ src, ...rest }: { src: string }) => {
if (src.includes('glama.ai')) return;
img: ({ src, ...rest }) => {
// FIXME ignore experimental blob image prop passing
if (typeof src !== 'string') return null;
if (src.includes('glama.ai')) return null;
// eslint-disable-next-line @next/next/no-img-element
if (src && src.startsWith('http')) return <img src={src} {...rest} />;
return;
if (src.startsWith('http')) return <img src={src} {...rest} />;
return null;
},
}}
enableImageGallery={false}

View File

@@ -1,12 +1,13 @@
'use client';
import { type NetworkProxySettings } from '@lobechat/electron-client-ipc';
import { Alert, Block, Flexbox, Skeleton, Text , Button } from '@lobehub/ui';
import { App, Divider, Form, Input, Radio, Space, Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { Alert, Flexbox, Form, type FormGroupItemType, Icon, Skeleton } from '@lobehub/ui';
import { Form as AntdForm, Button, Input, Radio, Space, Switch } from 'antd';
import { Loader2Icon } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FORM_STYLE } from '@/const/layoutTokens';
import { desktopSettingsService } from '@/services/electron/settings';
import { useElectronStore } from '@/store/electron';
@@ -19,15 +20,15 @@ interface ProxyTestResult {
const ProxyForm = () => {
const { t } = useTranslation('electron');
const [form] = Form.useForm();
const { message } = App.useApp();
const [testUrl, setTestUrl] = useState('https://www.google.com');
const [isTesting, setIsTesting] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [testResult, setTestResult] = useState<ProxyTestResult | null>(null);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
const [loading, setLoading] = useState(false);
const isEnableProxy = Form.useWatch('enableProxy', form);
const proxyRequireAuth = Form.useWatch('proxyRequireAuth', form);
const isEnableProxy = AntdForm.useWatch('enableProxy', form);
const proxyRequireAuth = AntdForm.useWatch('proxyRequireAuth', form);
const [setProxySettings, useGetProxySettings] = useElectronStore((s) => [
s.setProxySettings,
@@ -44,19 +45,12 @@ const ProxyForm = () => {
// 监听表单变化
const handleValuesChange = useCallback(() => {
setLoading(true);
setHasUnsavedChanges(true);
setTestResult(null); // 清除之前的测试结果
setLoading(false);
}, []);
const updateFormValue = (value: any) => {
const preValues = form.getFieldsValue();
form.setFieldsValue(value);
const newValues = form.getFieldsValue();
if (isEqual(newValues, preValues)) return;
handleValuesChange();
};
// 保存配置
const handleSave = useCallback(async () => {
try {
@@ -64,15 +58,12 @@ const ProxyForm = () => {
const values = await form.validateFields();
await setProxySettings(values);
setHasUnsavedChanges(false);
message.success(t('proxy.saveSuccess'));
} catch (error) {
if (error instanceof Error) {
message.error(t('proxy.saveFailed', { error: error.message }));
}
} catch {
// validation error
} finally {
setIsSaving(false);
}
}, [form, t, message]);
}, [form, setProxySettings]);
// 重置配置
const handleReset = useCallback(() => {
@@ -107,240 +98,159 @@ const ProxyForm = () => {
success: false,
};
setTestResult(result);
message.error(t('proxy.testFailed'));
} finally {
setIsTesting(false);
}
}, [proxySettings, testUrl]);
}, [proxySettings, testUrl, form]);
if (isLoading) return <Skeleton />;
if (isLoading) return <Skeleton active paragraph={{ rows: 5 }} title={false} />;
const enableProxyGroup: FormGroupItemType = {
children: [
{
children: <Switch />,
desc: t('proxy.enableDesc'),
label: t('proxy.enable'),
layout: 'horizontal',
minWidth: undefined,
name: 'enableProxy',
valuePropName: 'checked',
},
],
extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
title: t('proxy.enable'),
};
const basicSettingsGroup: FormGroupItemType = {
children: [
{
children: (
<Radio.Group disabled={!isEnableProxy}>
<Radio value="http">HTTP</Radio>
<Radio value="https">HTTPS</Radio>
<Radio value="socks5">SOCKS5</Radio>
</Radio.Group>
),
label: t('proxy.type'),
minWidth: undefined,
name: 'proxyType',
},
{
children: <Input disabled={!isEnableProxy} placeholder="127.0.0.1" />,
desc: t('proxy.validation.serverRequired'),
label: t('proxy.server'),
name: 'proxyServer',
},
{
children: <Input disabled={!isEnableProxy} placeholder="7890" style={{ width: 120 }} />,
desc: t('proxy.validation.portRequired'),
label: t('proxy.port'),
name: 'proxyPort',
},
],
extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
title: t('proxy.basicSettings'),
};
const authGroup: FormGroupItemType = {
children: [
{
children: <Switch disabled={!isEnableProxy} />,
desc: t('proxy.authDesc'),
label: t('proxy.auth'),
layout: 'horizontal',
minWidth: undefined,
name: 'proxyRequireAuth',
valuePropName: 'checked',
},
...(proxyRequireAuth && isEnableProxy
? [
{
children: <Input placeholder={t('proxy.username_placeholder')} />,
label: t('proxy.username'),
name: 'proxyUsername',
},
{
children: <Input.Password placeholder={t('proxy.password_placeholder')} />,
label: t('proxy.password'),
name: 'proxyPassword',
},
]
: []),
],
extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
title: t('proxy.authSettings'),
};
const testGroup: FormGroupItemType = {
children: [
{
children: (
<Flexbox gap={8}>
<Space.Compact style={{ width: '100%' }}>
<Input
onChange={(e) => setTestUrl(e.target.value)}
placeholder={t('proxy.testUrlPlaceholder')}
style={{ flex: 1 }}
value={testUrl}
/>
<Button loading={isTesting} onClick={handleTest} type="default">
{t('proxy.testButton')}
</Button>
</Space.Compact>
{/* 测试结果显示 */}
{!testResult ? null : testResult.success ? (
<Alert
closable
title={
<Flexbox align="center" gap={8} horizontal>
{t('proxy.testSuccessWithTime', { time: testResult.responseTime })}
</Flexbox>
}
type={'success'}
/>
) : (
<Alert
closable
title={
<Flexbox align="center" gap={8} horizontal>
{t('proxy.testFailed')}: {testResult.message}
</Flexbox>
}
type={'error'}
variant={'outlined'}
/>
)}
</Flexbox>
),
desc: t('proxy.testDescription'),
label: t('proxy.testUrl'),
minWidth: undefined,
},
],
extra: loading && <Icon icon={Loader2Icon} size={16} spin style={{ opacity: 0.5 }} />,
title: t('proxy.connectionTest'),
};
return (
<Form
disabled={isSaving}
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
requiredMark={false}
>
<Flexbox gap={24}>
{/* 基本代理设置 */}
<Block
paddingBlock={16}
paddingInline={24}
style={{ borderRadius: 12 }}
variant={'outlined'}
>
<Form.Item name="enableProxy" noStyle valuePropName="checked">
<Flexbox align={'center'} horizontal justify={'space-between'}>
<Flexbox>
<Text as={'h4'}>{t('proxy.enable')}</Text>
<Text type={'secondary'}>{t('proxy.enableDesc')}</Text>
</Flexbox>
<Switch
checked={isEnableProxy}
onChange={(checked) => {
updateFormValue({ enableProxy: checked });
}}
/>
</Flexbox>
</Form.Item>
</Block>
{/* 认证设置 */}
<Block
paddingBlock={16}
paddingInline={24}
style={{ borderRadius: 12 }}
variant={'outlined'}
>
<Flexbox gap={24}>
<Flexbox>
<Text as={'h4'}>{t('proxy.basicSettings')}</Text>
<Text type={'secondary'}>{t('proxy.basicSettingsDesc')}</Text>
</Flexbox>
<Flexbox>
<Form.Item
dependencies={['enableProxy']}
label={t('proxy.type')}
name="proxyType"
rules={[
({ getFieldValue }) => ({
message: t('proxy.validation.typeRequired'),
required: getFieldValue('enableProxy'),
}),
]}
>
<Radio.Group disabled={!form.getFieldValue('enableProxy')}>
<Radio value="http">HTTP</Radio>
<Radio value="https">HTTPS</Radio>
<Radio value="socks5">SOCKS5</Radio>
</Radio.Group>
</Form.Item>
<Space.Compact style={{ width: '100%' }}>
<Form.Item
dependencies={['enableProxy']}
label={t('proxy.server')}
name="proxyServer"
rules={[
({ getFieldValue }) => ({
message: t('proxy.validation.serverRequired'),
required: getFieldValue('enableProxy'),
}),
{
message: t('proxy.validation.serverInvalid'),
pattern:
/^((25[0-5]|2[0-4]\d|[01]?\d{1,2})\.){3}(25[0-5]|2[0-4]\d|[01]?\d{1,2})$|^[\dA-Za-z]([\dA-Za-z-]*[\dA-Za-z])?(\.[\dA-Za-z]([\dA-Za-z-]*[\dA-Za-z])?)*$/,
},
]}
style={{ flex: 1, marginBottom: 0 }}
>
<Input disabled={!form.getFieldValue('enableProxy')} placeholder="127.0.0.1" />
</Form.Item>
<Form.Item
dependencies={['enableProxy']}
label={t('proxy.port')}
name="proxyPort"
rules={[
({ getFieldValue }) => ({
message: t('proxy.validation.portRequired'),
required: getFieldValue('enableProxy'),
}),
{
message: t('proxy.validation.portInvalid'),
pattern:
/^([1-9]\d{0,3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$/,
},
]}
style={{ marginBottom: 0, width: 120 }}
>
<Input disabled={!form.getFieldValue('enableProxy')} placeholder="7890" />
</Form.Item>
</Space.Compact>
</Flexbox>
<Divider size={'small'} />
<Flexbox gap={12}>
<Form.Item
dependencies={['enableProxy']}
name="proxyRequireAuth"
noStyle
valuePropName="checked"
>
<Flexbox align={'center'} horizontal justify={'space-between'}>
<Flexbox>
<Text as={'h5'}>{t('proxy.auth')}</Text>
<Text type={'secondary'}>{t('proxy.authDesc')}</Text>
</Flexbox>
<Switch
checked={proxyRequireAuth}
disabled={!isEnableProxy}
onChange={(checked) => {
updateFormValue({ proxyRequireAuth: checked });
}}
/>
</Flexbox>
</Form.Item>
<Form.Item
dependencies={['proxyRequireAuth', 'enableProxy']}
label={t('proxy.username')}
name="proxyUsername"
rules={[
({ getFieldValue }) => ({
message: t('proxy.validation.usernameRequired'),
required: getFieldValue('proxyRequireAuth') && getFieldValue('enableProxy'),
}),
]}
style={{
display:
form.getFieldValue('proxyRequireAuth') && form.getFieldValue('enableProxy')
? 'block'
: 'none',
}}
>
<Input placeholder={t('proxy.username_placeholder')} />
</Form.Item>
<Form.Item
dependencies={['proxyRequireAuth', 'enableProxy']}
label={t('proxy.password')}
name="proxyPassword"
rules={[
({ getFieldValue }) => ({
message: t('proxy.validation.passwordRequired'),
required: getFieldValue('proxyRequireAuth') && getFieldValue('enableProxy'),
}),
]}
style={{
display:
form.getFieldValue('proxyRequireAuth') && form.getFieldValue('enableProxy')
? 'block'
: 'none',
}}
>
<Input.Password placeholder={t('proxy.password_placeholder')} />
</Form.Item>
</Flexbox>
</Flexbox>
</Block>
{/* 连接测试 */}
<Block
paddingBlock={16}
paddingInline={24}
style={{ borderRadius: 12 }}
variant={'outlined'}
>
<Flexbox gap={24}>
<Flexbox>
<Text as={'h4'}>{t('proxy.connectionTest')}</Text>
<Text type={'secondary'}>{t('proxy.testDescription')}</Text>
</Flexbox>
<Form.Item label={t('proxy.testUrl')}>
<Flexbox gap={8}>
<Space.Compact style={{ width: '100%' }}>
<Input
onChange={(e) => setTestUrl(e.target.value)}
placeholder={t('proxy.testUrlPlaceholder')}
style={{ flex: 1 }}
value={testUrl}
/>
<Button loading={isTesting} onClick={handleTest} type="default">
{t('proxy.testButton')}
</Button>
</Space.Compact>
{/* 测试结果显示 */}
{!testResult ? null : testResult.success ? (
<Alert
closable
title={
<Flexbox align="center" gap={8} horizontal>
{t('proxy.testSuccessWithTime', { time: testResult.responseTime })}
</Flexbox>
}
type={'success'}
/>
) : (
<Alert
closable
title={
<Flexbox align="center" gap={8} horizontal>
{t('proxy.testFailed')}: {testResult.message}
</Flexbox>
}
type={'error'}
variant={'outlined'}
/>
)}
</Flexbox>
</Form.Item>
</Flexbox>
</Block>
{/* 操作按钮 */}
<Space>
<Flexbox gap={24}>
<Form
collapsible={false}
form={form}
initialValues={proxySettings}
items={[enableProxyGroup, basicSettingsGroup, authGroup, testGroup]}
itemsType={'group'}
onValuesChange={handleValuesChange}
variant={'filled'}
{...FORM_STYLE}
/>
<Flexbox align="end" justify="flex-end">
{hasUnsavedChanges && (
<span style={{ color: 'var(--ant-color-warning)', marginBottom: 8 }}>
{t('proxy.unsavedChanges')}
</span>
)}
<Flexbox gap={8} horizontal>
<Button
disabled={!hasUnsavedChanges}
loading={isSaving}
@@ -349,19 +259,12 @@ const ProxyForm = () => {
>
{t('proxy.saveButton')}
</Button>
<Button disabled={!hasUnsavedChanges || isSaving} onClick={handleReset}>
{t('proxy.resetButton')}
</Button>
{hasUnsavedChanges && (
<Text style={{ marginLeft: 8 }} type="warning">
{t('proxy.unsavedChanges')}
</Text>
)}
</Space>
</Flexbox>
</Flexbox>
</Form>
</Flexbox>
);
};

View File

@@ -9,9 +9,7 @@ const Page = () => {
return (
<>
<SettingHeader title={t('tab.proxy')} />
<div style={{ maxWidth: '1024px', width: '100%' }}>
<ProxyForm />
</div>
<ProxyForm />
</>
);
};

View File

@@ -30,6 +30,7 @@ export const useMarkdown = (id: string): Partial<MarkdownProps> => {
() =>
({
components: Object.fromEntries(
// @ts-expect-error
markdownElements.map((element) => {
const Component = element.Component;
return [element.tag, (props: any) => <Component {...props} id={id} />];

View File

@@ -1,12 +1,13 @@
'use client';
import { DEFAULT_INBOX_AVATAR } from '@lobechat/const';
import { nanoid } from '@lobechat/utils';
import { HIDE_TOOLBAR_COMMAND, type IEditor } from '@lobehub/editor';
import { type ChatInputActionsProps } from '@lobehub/editor/react';
import { Block } from '@lobehub/ui';
import { Avatar, Block } from '@lobehub/ui';
import { createStaticStyles, cssVar } from 'antd-style';
import { BotIcon } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useFileStore } from '@/store/file';
import { useGlobalStore } from '@/store/global';
@@ -23,11 +24,14 @@ const styles = createStaticStyles(({ css }) => ({
}));
export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActionsProps['items'] => {
const { t } = useTranslation('common');
const addSelectionContext = useFileStore((s) => s.addChatContextSelection);
return useMemo(() => {
if (!editor) return [];
const label = t('cmdk.askLobeAI');
return [
{
children: (
@@ -82,14 +86,14 @@ export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActions
paddingInline={12}
variant="borderless"
>
<BotIcon />
<span>Ask Copilot</span>
<Avatar avatar={DEFAULT_INBOX_AVATAR} shape="square" size={16} />
<span>{label}</span>
</Block>
),
key: 'ask-copilot',
label: 'Ask Copilot',
label,
onClick: () => {},
},
];
}, [addSelectionContext, editor]);
}, [addSelectionContext, editor, t]);
};

View File

@@ -1,7 +1,7 @@
'use client';
import { ActionIcon, Avatar, Dropdown, Text } from '@lobehub/ui';
import { ArrowLeftIcon, BotMessageSquareIcon, MoreHorizontal } from 'lucide-react';
import { ActionIcon, Avatar, DropdownMenu, Text } from '@lobehub/ui';
import { ArrowLeftIcon, MoreHorizontal } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -49,18 +49,20 @@ const Header = memo(() => {
}
right={
<>
<ToggleRightPanelButton icon={BotMessageSquareIcon} showActive={true} />
{/* Three-dot menu */}
<Dropdown
menu={{
items: menuItems,
style: { minWidth: 200 },
}}
<DropdownMenu
items={menuItems}
nativeButton={false}
placement="bottomRight"
trigger={['click']}
popupProps={{
style: {
minWidth: 200,
},
}}
>
<ActionIcon icon={MoreHorizontal} size={DESKTOP_HEADER_ICON_SIZE} />
</Dropdown>
</DropdownMenu>
<ToggleRightPanelButton hideWhenExpanded showActive={false} />
</>
}
/>

View File

@@ -1,5 +1,5 @@
import { Flexbox, Icon } from '@lobehub/ui';
import { App, Switch } from 'antd';
import { type DropdownItem, Icon } from '@lobehub/ui';
import { App } from 'antd';
import { cssVar, useResponsive } from 'antd-style';
import dayjs from 'dayjs';
import { CopyPlus, Download, Link2, Trash2 } from 'lucide-react';
@@ -75,25 +75,17 @@ export const useMenu = (): { menuItems: any[] } => {
}
};
const menuItems = useMemo(
() => [
const menuItems = useMemo<DropdownItem[]>(() => {
const items: DropdownItem[] = [
...(showViewModeSwitch
? [
{
checked: wideScreen,
key: 'full-width',
label: (
<Flexbox align="center" horizontal justify="space-between">
<span>{t('viewMode.fullWidth', { ns: 'chat' })}</span>
<Switch
checked={wideScreen}
onChange={toggleWideScreen}
onClick={(checked, event) => {
event.stopPropagation();
}}
size="small"
/>
</Flexbox>
),
label: t('viewMode.fullWidth', { ns: 'chat' }),
onCheckedChange: toggleWideScreen,
type: 'checkbox' as const,
},
{
type: 'divider' as const,
@@ -140,38 +132,43 @@ export const useMenu = (): { menuItems: any[] } => {
key: 'export',
label: t('pageEditor.menu.export'),
},
{
type: 'divider' as const,
},
{
disabled: true,
key: 'page-info',
label: (
<div style={{ color: cssVar.colorTextTertiary, fontSize: 12, lineHeight: 1.6 }}>
<div>
{lastUpdatedTime
? t('pageEditor.editedAt', {
time: dayjs(lastUpdatedTime).format('MMMM D, YYYY [at] h:mm A'),
})
: ''}
];
if (lastUpdatedTime) {
items.push(
{
type: 'divider' as const,
},
{
disabled: true,
key: 'page-info',
label: (
<div style={{ color: cssVar.colorTextTertiary, fontSize: 12, lineHeight: 1.6 }}>
<div>
{lastUpdatedTime
? t('pageEditor.editedAt', {
time: dayjs(lastUpdatedTime).format('MMMM D, YYYY [at] h:mm A'),
})
: ''}
</div>
</div>
</div>
),
},
],
[
lastUpdatedTime,
storeApi,
t,
message,
modal,
wideScreen,
toggleWideScreen,
showViewModeSwitch,
handleDuplicate,
handleExportMarkdown,
],
);
),
},
);
}
return items;
}, [
lastUpdatedTime,
storeApi,
t,
message,
modal,
wideScreen,
toggleWideScreen,
showViewModeSwitch,
handleDuplicate,
handleExportMarkdown,
]);
return { menuItems };
};

View File

@@ -15,6 +15,7 @@ import { HotkeyEnum } from '@/types/hotkey';
export const TOGGLE_BUTTON_ID = 'toggle_right_panel_button';
interface ToggleRightPanelButtonProps {
hideWhenExpanded?: boolean;
icon?: ActionIconProps['icon'];
showActive?: boolean;
size?: ActionIconProps['size'];
@@ -22,7 +23,7 @@ interface ToggleRightPanelButtonProps {
}
const ToggleRightPanelButton = memo<ToggleRightPanelButtonProps>(
({ title, showActive, icon, size }) => {
({ title, showActive, icon, hideWhenExpanded, size }) => {
const [expand, togglePanel] = useGlobalStore((s) => [
systemStatusSelectors.showRightPanel(s),
s.toggleRightPanel,
@@ -31,6 +32,7 @@ const ToggleRightPanelButton = memo<ToggleRightPanelButtonProps>(
const { t } = useTranslation(['chat', 'hotkey']);
if (hideWhenExpanded && expand) return null;
return (
<ActionIcon
active={showActive ? expand : undefined}

40
tests/mocks/lru_map.ts Normal file
View File

@@ -0,0 +1,40 @@
class LRUMap<K, V> {
private map = new Map<K, V>();
private limit: number;
constructor(limit = 0) {
this.limit = limit;
}
get size() {
return this.map.size;
}
get(key: K) {
return this.map.get(key);
}
set(key: K, value: V) {
if (!this.map.has(key) && this.limit > 0 && this.map.size >= this.limit) {
const oldest = this.map.keys().next().value as K | undefined;
if (oldest !== undefined) this.map.delete(oldest);
}
this.map.set(key, value);
return this;
}
delete(key: K) {
const value = this.map.get(key);
this.map.delete(key);
return value;
}
clear() {
this.map.clear();
}
}
export { LRUMap };
export default { LRUMap };

View File

@@ -56,6 +56,7 @@ export default defineConfig({
'@/const': resolve(__dirname, './packages/const/src'),
'@': resolve(__dirname, './src'),
'~test-utils': resolve(__dirname, './tests/utils.tsx'),
'lru_map': resolve(__dirname, './tests/mocks/lru_map'),
/* eslint-enable */
},
coverage: {
@@ -94,7 +95,14 @@ export default defineConfig({
globals: true,
server: {
deps: {
inline: ['vitest-canvas-mock', '@lobehub/ui', '@lobehub/fluent-emoji'],
inline: [
'vitest-canvas-mock',
'@lobehub/ui',
'@lobehub/fluent-emoji',
'@pierre/diffs',
'@pierre/diffs/react',
'lru_map',
],
},
},
setupFiles: join(__dirname, './tests/setup.ts'),