feat: minApp supports show/hide, add to the sidebar

This commit is contained in:
hxp0618 2025-01-10 14:48:28 +08:00 committed by kangfenmao
parent c45fc2bbad
commit fc3d15fae8
13 changed files with 521 additions and 47 deletions

View File

@ -202,11 +202,19 @@ const EmptyView = styled.div`
export default class MinApp { export default class MinApp {
static topviewId = 0 static topviewId = 0
static onClose = () => {} static onClose = () => {}
static close() { static isOpening = false
TopView.hide('MinApp')
store.dispatch(setMinappShow(false)) static async start(app: MinAppType) {
} if (this.isOpening) return
static start(app: MinAppType) { this.isOpening = true
try {
// 先关闭现有的小程序
await this.close()
// 确保 webview 完全卸载
await new Promise(resolve => setTimeout(resolve, 100))
store.dispatch(setMinappShow(true)) store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => { return new Promise<any>((resolve) => {
TopView.show( TopView.show(
@ -220,5 +228,14 @@ export default class MinApp {
'MinApp' 'MinApp'
) )
}) })
} finally {
this.isOpening = false
}
}
static close() {
if (!this.isOpening) return
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
} }
} }

View File

@ -1,6 +1,7 @@
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons' import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env' import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { getAllMinApps } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime' import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
@ -21,8 +22,9 @@ const Sidebar: FC = () => {
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { windowStyle, sidebarIcons } = useSettings() const { windowStyle, sidebarIcons, miniAppIcons } = useSettings()
const { theme, toggleTheme } = useTheme() const { theme, toggleTheme } = useTheme()
const allApps = getAllMinApps()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '') const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
@ -72,6 +74,27 @@ const Sidebar: FC = () => {
}) })
} }
const renderPinnedApps = () => {
if (!miniAppIcons?.pinned) return null
const pinnedApps = allApps.filter((app) => miniAppIcons.pinned.includes(app.id))
return pinnedApps.map((app) => (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Icon onClick={() => MinApp.start(app)}>
<AppIcon
src={app.logo}
style={{
width: '20px',
height: '20px',
border: app.bodered ? '0.5px solid var(--color-border)' : 'none'
}}
/>
</Icon>
</StyledLink>
</Tooltip>
))
}
return ( return (
<Container <Container
id="app-sidebar" id="app-sidebar"
@ -81,7 +104,12 @@ const Sidebar: FC = () => {
}}> }}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenus>
<Menus onClick={MinApp.onClose}>{renderMainMenus()}</Menus> <ScrollContainer>
<Menus onClick={MinApp.onClose}>
{renderMainMenus()}
{renderPinnedApps()}
</Menus>
</ScrollContainer>
</MainMenus> </MainMenus>
<Menus onClick={MinApp.onClose}> <Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right"> <Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
@ -129,6 +157,7 @@ const AvatarImg = styled(Avatar)`
const MainMenus = styled.div` const MainMenus = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
overflow: hidden;
` `
const Menus = styled.div` const Menus = styled.div`
@ -182,4 +211,31 @@ const StyledLink = styled.div`
} }
` `
const AppIcon = styled.img`
border-radius: 6px;
`
const ScrollContainer = styled.div`
overflow-y: auto;
overflow-x: hidden;
height: 100%;
&::-webkit-scrollbar {
width: 0px;
}
&:hover::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
`
export default Sidebar export default Sidebar

View File

@ -264,7 +264,9 @@
"error.get_embedding_dimensions": "Failed to get embedding dimensions" "error.get_embedding_dimensions": "Failed to get embedding dimensions"
}, },
"minapp": { "minapp": {
"title": "MinApp" "title": "MinApp",
"sidebar.add.title": "Add minAPP to sidebar",
"sidebar.remove.title": "Remove minAPP from sidebar"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.", "keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
@ -405,6 +407,10 @@
"display.sidebar.disabled": "Hide my sidebar icons", "display.sidebar.disabled": "Hide my sidebar icons",
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding", "display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
"display.sidebar.empty": "Drag the hidden feature from the left side here", "display.sidebar.empty": "Drag the hidden feature from the left side here",
"display.minApp.title": "MinApp Settings",
"display.minApp.visible": "Visible MinApp",
"display.minApp.disabled": "Hidden MinApp",
"display.minApp.empty": "Drag minApp from the left to hide them here",
"display.topic.title": "Topic Settings", "display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS", "display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */", "display.custom.css.placeholder": "/* Put custom CSS here */",

View File

@ -262,7 +262,9 @@
"copy.success": "コピーしました!" "copy.success": "コピーしました!"
}, },
"minapp": { "minapp": {
"title": "ミニアプリ" "title": "ミニアプリ",
"sidebar.add.title": "ミニプログラムをサイドバーに追加",
"sidebar.remove.title": "サイドバーからアプレットを削除する"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分", "keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",
@ -406,6 +408,10 @@
"display.topic.title": "トピック設定", "display.topic.title": "トピック設定",
"display.custom.css": "カスタムCSS", "display.custom.css": "カスタムCSS",
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */", "display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
"display.minApp.title": "ミニプログラム表示設定",
"display.minApp.visible": "表示中ミニプログラム",
"display.minApp.disabled": "非表示ミニプログラム",
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
"input.auto_translate_with_space": "スペースを3回押して翻訳", "input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示", "messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け", "messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",

View File

@ -264,7 +264,9 @@
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания" "error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
}, },
"minapp": { "minapp": {
"title": "Встроенные приложения" "title": "Встроенные приложения",
"sidebar.add.title": "Добавить мини-программу на боковую панель",
"sidebar.remove.title": "Удалить апплет из боковой панели"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.", "keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
@ -405,6 +407,10 @@
"display.sidebar.disabled": "Скрыть значок на боковой панели", "display.sidebar.disabled": "Скрыть значок на боковой панели",
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие", "display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда", "display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
"display.minApp.title": "Настройки отображения мини программы",
"display.minApp.visible": "Отображаемый апплет",
"display.minApp.disabled": "скрытый апплет",
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
"display.topic.title": "Настройки топиков", "display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS", "display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */", "display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",

View File

@ -265,7 +265,9 @@
"error.get_embedding_dimensions": "获取嵌入维度失败" "error.get_embedding_dimensions": "获取嵌入维度失败"
}, },
"minapp": { "minapp": {
"title": "小程序" "title": "小程序",
"sidebar.add.title": "添加小程序到侧边栏",
"sidebar.remove.title": "从侧边栏移除小程序"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟", "keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟",
@ -406,6 +408,10 @@
"display.sidebar.disabled": "隐藏我的侧边栏图标", "display.sidebar.disabled": "隐藏我的侧边栏图标",
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏", "display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里", "display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
"display.minApp.title": "小程序显示设置",
"display.minApp.visible": "显示的小程序",
"display.minApp.disabled": "隐藏的小程序",
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
"display.topic.title": "话题设置", "display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS", "display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */", "display.custom.css.placeholder": "/* 这里写自定义CSS */",

View File

@ -264,7 +264,9 @@
"error.get_embedding_dimensions": "獲取嵌入維度失敗" "error.get_embedding_dimensions": "獲取嵌入維度失敗"
}, },
"minapp": { "minapp": {
"title": "小程序" "title": "小程序",
"sidebar.add.title": "新增小程式到側邊欄",
"sidebar.remove.title": "從側邊欄移除小程式"
}, },
"ollama": { "ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。", "keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
@ -409,6 +411,14 @@
"display.custom.css": "自定義 CSS", "display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */", "display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯", "input.auto_translate_with_space": "快速敲擊3次空格翻譯",
"display": {
"minApp": {
"title": "小程序顯示設定",
"visible": "顯示的小程序",
"disabled": "隱藏的小程序",
"empty": "把要隱藏的小程序從左側拖拽到這裡"
}
},
"messages.divider": "訊息間顯示分隔線", "messages.divider": "訊息間顯示分隔線",
"messages.input.paste_long_text_as_file": "將長文本貼上為檔案", "messages.input.paste_long_text_as_file": "將長文本貼上為檔案",
"messages.input.send_shortcuts": "發送快捷鍵", "messages.input.send_shortcuts": "發送快捷鍵",

View File

@ -1,8 +1,14 @@
import MinApp from '@renderer/components/MinApp' import MinApp from '@renderer/components/MinApp'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMiniAppIcons } from '@renderer/store/settings'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
app: MinAppType app: MinAppType
onClick?: () => void onClick?: () => void
@ -10,12 +16,37 @@ interface Props {
} }
const App: FC<Props> = ({ app, onClick, size = 60 }) => { const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { miniAppIcons } = useAppSelector((state) => state.settings)
const isPinned = miniAppIcons?.pinned.includes(app.id)
const handleClick = () => { const handleClick = () => {
MinApp.start(app) MinApp.start(app)
onClick?.() onClick?.()
} }
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
const newPinned = isPinned
? miniAppIcons.pinned.filter((id) => id !== app.id)
: [...(miniAppIcons.pinned || []), app.id]
dispatch(
setMiniAppIcons({
...miniAppIcons,
pinned: newPinned
})
)
}
}
]
return ( return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Container onClick={handleClick}> <Container onClick={handleClick}>
<AppIcon <AppIcon
src={app.logo} src={app.logo}
@ -27,6 +58,7 @@ const App: FC<Props> = ({ app, onClick, size = 60 }) => {
/> />
<AppTitle>{app.name}</AppTitle> <AppTitle>{app.name}</AppTitle>
</Container> </Container>
</Dropdown>
) )
} }

View File

@ -2,9 +2,10 @@ import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout' import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps' import { getAllMinApps } from '@renderer/config/minapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { Empty, Input } from 'antd' import { Empty, Input } from 'antd'
import { isEmpty } from 'lodash' import { isEmpty } from 'lodash'
import { FC, useMemo, useState } from 'react' import React, { FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -13,16 +14,34 @@ import App from './App'
const AppsPage: FC = () => { const AppsPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const apps = useMemo(() => getAllMinApps(), []) const { miniAppIcons } = useSettings()
const allApps = useMemo(() => getAllMinApps(), [])
// 只显示可见的小程序
const visibleApps = useMemo(() => {
if (!miniAppIcons?.visible) return allApps
return allApps.filter((app) => miniAppIcons.visible.includes(app.id))
}, [allApps, miniAppIcons?.visible])
const filteredApps = search const filteredApps = search
? apps.filter( ? visibleApps.filter(
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase()) (app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
) )
: apps : visibleApps
// Calculate the required number of lines
const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing)
const rowCount = Math.ceil(filteredApps.length / itemsPerRow)
// Each line height is 85px (60px icon + 5px margin + 12px text + spacing)
const containerHeight = rowCount * 85 + (rowCount - 1) * 25 // 25px is the line spacing.
// Disable right-click menu in blank area
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault()
}
return ( return (
<Container> <Container onContextMenu={handleContextMenu}>
<Navbar> <Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('minapp.title')} {t('minapp.title')}
@ -40,7 +59,7 @@ const AppsPage: FC = () => {
</NavbarCenter> </NavbarCenter>
</Navbar> </Navbar>
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<AppsContainer> <AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => ( {filteredApps.map((app) => (
<App key={app.id} app={app} /> <App key={app.id} app={app} />
))} ))}
@ -68,7 +87,7 @@ const ContentContainer = styled.div`
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
overflow-y: scroll; overflow-y: auto;
padding: 50px; padding: 50px;
` `
@ -77,9 +96,7 @@ const AppsContainer = styled.div`
min-width: 0; min-width: 0;
max-width: 930px; max-width: 930px;
width: 100%; width: 100%;
max-height: 520px; grid-template-columns: repeat(auto-fill, 90px);
min-height: 520px;
grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
gap: 25px; gap: 25px;
justify-content: center; justify-content: center;
` `

View File

@ -3,9 +3,11 @@ import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { import {
DEFAULT_MINIAPP_ICONS,
DEFAULT_SIDEBAR_ICONS, DEFAULT_SIDEBAR_ICONS,
setClickAssistantToShowTopic, setClickAssistantToShowTopic,
setCustomCss, setCustomCss,
setMiniAppIcons,
setShowTopicTime, setShowTopicTime,
setSidebarIcons setSidebarIcons
} from '@renderer/store/settings' } from '@renderer/store/settings'
@ -16,6 +18,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
import MiniAppIconsManager from './MiniAppIconsManager'
import SidebarIconsManager from './SidebarIconsManager' import SidebarIconsManager from './SidebarIconsManager'
const DisplaySettings: FC = () => { const DisplaySettings: FC = () => {
@ -29,7 +32,8 @@ const DisplaySettings: FC = () => {
clickAssistantToShowTopic, clickAssistantToShowTopic,
showTopicTime, showTopicTime,
customCss, customCss,
sidebarIcons sidebarIcons,
miniAppIcons
} = useSettings() } = useSettings()
const { theme: themeMode } = useTheme() const { theme: themeMode } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
@ -37,6 +41,8 @@ const DisplaySettings: FC = () => {
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS) const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || []) const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
const [visibleMiniApps, setVisibleMiniApps] = useState(miniAppIcons?.visible || DEFAULT_MINIAPP_ICONS)
const [disabledMiniApps, setDisabledMiniApps] = useState(miniAppIcons?.disabled || [])
// 使用useCallback优化回调函数 // 使用useCallback优化回调函数
const handleWindowStyleChange = useCallback( const handleWindowStyleChange = useCallback(
@ -52,6 +58,18 @@ const DisplaySettings: FC = () => {
dispatch(setSidebarIcons({ visible: DEFAULT_SIDEBAR_ICONS, disabled: [] })) dispatch(setSidebarIcons({ visible: DEFAULT_SIDEBAR_ICONS, disabled: [] }))
}, [dispatch]) }, [dispatch])
const handleResetMinApps = useCallback(() => {
setVisibleMiniApps(DEFAULT_MINIAPP_ICONS)
setDisabledMiniApps([])
dispatch(
setMiniAppIcons({
visible: DEFAULT_MINIAPP_ICONS,
disabled: [],
pinned: miniAppIcons?.pinned || []
})
)
}, [dispatch, miniAppIcons?.pinned])
return ( return (
<SettingContainer theme={themeMode}> <SettingContainer theme={themeMode}>
<SettingGroup theme={theme}> <SettingGroup theme={theme}>
@ -142,6 +160,22 @@ const DisplaySettings: FC = () => {
}} }}
/> />
</SettingGroup> </SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle
style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<span>{t('settings.display.minApp.title')}</span>
<ResetButtonWrapper>
<Button onClick={handleResetMinApps}>{t('common.reset')}</Button>
</ResetButtonWrapper>
</SettingTitle>
<SettingDivider />
<MiniAppIconsManager
visibleMiniApps={visibleMiniApps}
disabledMiniApps={disabledMiniApps}
setVisibleMiniApps={setVisibleMiniApps}
setDisabledMiniApps={setDisabledMiniApps}
/>
</SettingGroup>
</SettingContainer> </SettingContainer>
) )
} }

View File

@ -0,0 +1,256 @@
import { CloseOutlined } from '@ant-design/icons'
import {
DragDropContext,
Draggable,
DraggableProvided,
Droppable,
DroppableProvided,
DropResult
} from '@hello-pangea/dnd'
import { getAllMinApps } from '@renderer/config/minapps'
import { useAppDispatch } from '@renderer/store'
import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { MinAppIcon, setMiniAppIcons } from '../../../store/settings'
interface MiniAppManagerProps {
visibleMiniApps: MinAppIcon[]
disabledMiniApps: MinAppIcon[]
setVisibleMiniApps: (programs: MinAppIcon[]) => void
setDisabledMiniApps: (programs: MinAppIcon[]) => void
}
// 将可复用的类型和常量提取出来
type ListType = 'visible' | 'disabled'
interface AppInfo {
name: string
logo?: string
}
const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
visibleMiniApps,
disabledMiniApps,
setVisibleMiniApps,
setDisabledMiniApps
}) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const allApps = useMemo(() => getAllMinApps(), [])
// 创建 app 信息的 Map 缓存
const appInfoMap = useMemo(() => {
return allApps.reduce(
(acc, app) => {
acc[String(app.id)] = { name: app.name, logo: app.logo }
return acc
},
{} as Record<string, AppInfo>
)
}, [allApps])
const getAppInfo = useCallback(
(id: MinAppIcon) => {
return appInfoMap[String(id)] || { name: id, logo: '' }
},
[appInfoMap]
)
const handleListUpdate = useCallback(
(visible: MinAppIcon[], disabled: MinAppIcon[]) => {
setVisibleMiniApps(visible)
setDisabledMiniApps(disabled)
dispatch(setMiniAppIcons({ visible, disabled, pinned: [] }))
},
[dispatch, setVisibleMiniApps, setDisabledMiniApps]
)
const onDragEnd = useCallback(
(result: DropResult) => {
const { source, destination } = result
if (!destination) return
const sourceList = source.droppableId === 'visible' ? visibleMiniApps : disabledMiniApps
const destList = destination.droppableId === 'visible' ? visibleMiniApps : disabledMiniApps
if (source.droppableId === destination.droppableId) {
const newList = [...sourceList]
const [removed] = newList.splice(source.index, 1)
newList.splice(destination.index, 0, removed)
handleListUpdate(
source.droppableId === 'visible' ? newList : visibleMiniApps,
source.droppableId === 'disabled' ? newList : disabledMiniApps
)
} else {
const sourceNewList = [...sourceList]
const [removed] = sourceNewList.splice(source.index, 1)
const destNewList = [...destList]
destNewList.splice(destination.index, 0, removed)
handleListUpdate(
destination.droppableId === 'visible' ? destNewList : sourceNewList,
destination.droppableId === 'disabled' ? destNewList : sourceNewList
)
}
},
[visibleMiniApps, disabledMiniApps, handleListUpdate]
)
const onMoveMiniApp = useCallback(
(program: MinAppIcon, fromList: ListType) => {
const isMovingToVisible = fromList === 'disabled'
const newVisible = isMovingToVisible
? [...visibleMiniApps, program]
: visibleMiniApps.filter((p) => p !== program)
const newDisabled = isMovingToVisible
? disabledMiniApps.filter((p) => p !== program)
: [...disabledMiniApps, program]
handleListUpdate(newVisible, newDisabled)
},
[visibleMiniApps, disabledMiniApps, handleListUpdate]
)
const renderProgramItem = (program: MinAppIcon, provided: DraggableProvided, listType: ListType) => {
const { name, logo } = getAppInfo(program)
return (
<ProgramItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<ProgramContent>
<AppLogo src={logo} alt={name} />
<span>{name}</span>
</ProgramContent>
<CloseButton onClick={() => onMoveMiniApp(program, listType)}>
<CloseOutlined />
</CloseButton>
</ProgramItem>
)
}
return (
<DragDropContext onDragEnd={onDragEnd}>
<ProgramSection>
{(['visible', 'disabled'] as const).map((listType) => (
<ProgramColumn key={listType}>
<h4>{t(`settings.display.minApp.${listType}`)}</h4>
<Droppable droppableId={listType}>
{(provided: DroppableProvided) => (
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
<ScrollContainer>
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
<Draggable key={program} draggableId={String(program)} index={index}>
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
</Draggable>
))}
{disabledMiniApps.length === 0 && listType === 'disabled' && (
<EmptyPlaceholder>{t('settings.display.minApp.empty')}</EmptyPlaceholder>
)}
{provided.placeholder}
</ScrollContainer>
</ProgramList>
)}
</Droppable>
</ProgramColumn>
))}
</ProgramSection>
</DragDropContext>
)
}
const AppLogo = styled.img`
width: 16px;
height: 16px;
border-radius: 4px;
object-fit: contain;
`
const ScrollContainer = styled.div`
overflow-y: auto;
height: 100%;
`
const ProgramSection = styled.div`
display: flex;
gap: 20px;
padding: 10px;
background: var(--color-background);
`
const ProgramColumn = styled.div`
flex: 1;
h4 {
margin-bottom: 10px;
color: var(--color-text);
font-weight: normal;
}
`
const ProgramList = styled.div`
height: 365px;
min-height: 365px;
padding: 10px;
background: var(--color-background-soft);
border-radius: 8px;
border: 1px solid var(--color-border);
display: flex;
flex-direction: column;
overflow-y: hidden;
`
const ProgramItem = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
margin-bottom: 8px;
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: move;
`
const ProgramContent = styled.div`
display: flex;
align-items: center;
gap: 10px;
.iconfont {
font-size: 16px;
color: var(--color-text);
}
span {
color: var(--color-text);
}
`
const CloseButton = styled.div`
cursor: pointer;
color: var(--color-text-2);
opacity: 0;
transition: all 0.2s;
&:hover {
color: var(--color-text);
}
${ProgramItem}:hover & {
opacity: 1;
}
`
const EmptyPlaceholder = styled.div`
display: flex;
flex: 1;
align-items: center;
justify-content: center;
color: var(--color-text-2);
text-align: center;
padding: 20px;
font-size: 14px;
`
export default MiniAppIconsManager

View File

@ -9,7 +9,7 @@ import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist' import { createMigrate } from 'redux-persist'
import { RootState } from '.' import { RootState } from '.'
import { DEFAULT_SIDEBAR_ICONS } from './settings' import { DEFAULT_MINIAPP_ICONS, DEFAULT_SIDEBAR_ICONS } from './settings'
const migrateConfig = { const migrateConfig = {
'2': (state: RootState) => { '2': (state: RootState) => {
@ -789,6 +789,11 @@ const migrateConfig = {
visible: DEFAULT_SIDEBAR_ICONS, visible: DEFAULT_SIDEBAR_ICONS,
disabled: [] disabled: []
} }
state.settings.miniAppIcons = {
visible: DEFAULT_MINIAPP_ICONS,
disabled: [],
pinned: []
}
return state return state
} }
} }

View File

@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getAllMinApps } from '@renderer/config/minapps'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts' import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { CodeStyleVarious, LanguageVarious, ThemeMode } from '@renderer/types' import { CodeStyleVarious, LanguageVarious, ThemeMode } from '@renderer/types'
@ -15,6 +16,11 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'knowledge', 'knowledge',
'files' 'files'
] ]
const [minApps] = await Promise.all([getAllMinApps()])
export type MinAppIcon = (typeof minApps)[number]['id'] // 假设每个小程序对象有 type 字段
export const DEFAULT_MINIAPP_ICONS: MinAppIcon[] = minApps.map((app) => app.id)
export interface SettingsState { export interface SettingsState {
showAssistants: boolean showAssistants: boolean
@ -61,6 +67,11 @@ export interface SettingsState {
disabled: SidebarIcon[] disabled: SidebarIcon[]
} }
narrowMode: boolean narrowMode: boolean
miniAppIcons: {
visible: MinAppIcon[]
disabled: MinAppIcon[]
pinned: MinAppIcon[]
}
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@ -105,7 +116,12 @@ const initialState: SettingsState = {
visible: DEFAULT_SIDEBAR_ICONS, visible: DEFAULT_SIDEBAR_ICONS,
disabled: [] disabled: []
}, },
narrowMode: false narrowMode: false,
miniAppIcons: {
visible: DEFAULT_MINIAPP_ICONS,
disabled: [],
pinned: []
}
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -235,6 +251,12 @@ const settingsSlice = createSlice({
}, },
setNarrowMode: (state, action: PayloadAction<boolean>) => { setNarrowMode: (state, action: PayloadAction<boolean>) => {
state.narrowMode = action.payload state.narrowMode = action.payload
},
setMiniAppIcons: (
state,
action: PayloadAction<{ visible: MinAppIcon[]; disabled: MinAppIcon[]; pinned: MinAppIcon[] }>
) => {
state.miniAppIcons = action.payload
} }
} }
}) })
@ -280,7 +302,8 @@ export const {
setCustomCss, setCustomCss,
setTopicNamingPrompt, setTopicNamingPrompt,
setSidebarIcons, setSidebarIcons,
setNarrowMode setNarrowMode,
setMiniAppIcons
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer