From 43289845fc4007be4291abb8db4e6a6dc2b703f4 Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Fri, 13 Mar 2026 22:37:02 +0800 Subject: [PATCH] update UI --- src/renderer/src/App.tsx | 24 ++- .../src/components/AppList/AppList.tsx | 202 +++++++++++++++--- .../components/DomainManager/DomainForm.tsx | 12 +- .../components/DomainManager/DomainList.tsx | 99 +++++++-- .../DomainManager/DomainManager.tsx | 67 +++--- src/renderer/src/stores/domainStore.ts | 19 ++ src/renderer/src/stores/index.ts | 2 + src/renderer/src/stores/uiStore.ts | 102 +++++++++ 8 files changed, 440 insertions(+), 87 deletions(-) create mode 100644 src/renderer/src/stores/uiStore.ts diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index e25a488..efb26d0 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -27,6 +27,7 @@ import { import { createStyles } from "antd-style"; import zhCN from "antd/locale/zh_CN"; import { useDomainStore } from "@renderer/stores"; +import { useUIStore } from "@renderer/stores"; import { DomainManager } from "@renderer/components/DomainManager"; import { AppList } from "@renderer/components/AppList"; import { AppDetail } from "@renderer/components/AppDetail"; @@ -153,10 +154,15 @@ const App: React.FC = () => { } = theme.useToken(); const { currentDomain } = useDomainStore(); + const { + sidebarWidth, + siderCollapsed, + domainExpanded, + setSidebarWidth, + setSiderCollapsed, + setDomainExpanded, + } = useUIStore(); const [deployDialogOpen, setDeployDialogOpen] = React.useState(false); - const [domainExpanded, setDomainExpanded] = React.useState(true); - const [siderCollapsed, setSiderCollapsed] = React.useState(false); - const [siderWidth, setSiderWidth] = React.useState(DEFAULT_SIDER_WIDTH); const [isResizing, setIsResizing] = React.useState(false); const domainSectionHeight = domainExpanded @@ -170,7 +176,7 @@ const App: React.FC = () => { setIsResizing(true); const startX = e.clientX; - const startWidth = siderWidth; + const startWidth = sidebarWidth; const handleMouseMove = (moveEvent: MouseEvent) => { const delta = moveEvent.clientX - startX; @@ -178,7 +184,7 @@ const App: React.FC = () => { MAX_SIDER_WIDTH, Math.max(MIN_SIDER_WIDTH, startWidth + delta), ); - setSiderWidth(newWidth); + setSidebarWidth(newWidth); }; const handleMouseUp = () => { @@ -190,7 +196,7 @@ const App: React.FC = () => { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); }, - [siderWidth], + [sidebarWidth, setSidebarWidth], ); const toggleSider = () => { @@ -203,9 +209,9 @@ const App: React.FC = () => { {/* Left Sider - Domain List & App List */} {!siderCollapsed && ( <> @@ -260,7 +266,7 @@ const App: React.FC = () => { {/* Main Content */}
diff --git a/src/renderer/src/components/AppList/AppList.tsx b/src/renderer/src/components/AppList/AppList.tsx index eff2d2a..96a1867 100644 --- a/src/renderer/src/components/AppList/AppList.tsx +++ b/src/renderer/src/components/AppList/AppList.tsx @@ -1,18 +1,33 @@ /** * AppList Component - * Displays apps in a clickable list (no pagination) + * Displays apps in a clickable list with sorting and pinning */ import React from "react"; -import { List, Button, Input, Empty, Spin, Typography, Space } from "antd"; +import { + List, + Button, + Input, + Empty, + Spin, + Typography, + Space, + Tooltip, + Select, +} from "antd"; import { ReloadOutlined, SearchOutlined, AppstoreOutlined, + PushpinOutlined, + PushpinFilled, + SortAscendingOutlined, + SortDescendingOutlined, } from "@ant-design/icons"; import { createStyles } from "antd-style"; import { useAppStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores"; +import { useUIStore } from "@renderer/stores"; import type { AppDetail } from "@shared/types/kintone"; const { Text } = Typography; @@ -35,7 +50,7 @@ const useStyles = createStyles(({ token, css }) => ({ `, searchWrapper: css` flex: 1; - max-width: 300px; + max-width: 200px; `, content: css` flex: 1; @@ -68,6 +83,9 @@ const useStyles = createStyles(({ token, css }) => ({ background: ${token.colorPrimaryBgHover} !important; border-left: 3px solid ${token.colorPrimary} !important; `, + listItemPinned: css` + background: ${token.colorWarningBg} !important; + `, appInfo: css` display: flex; align-items: center; @@ -100,6 +118,18 @@ const useStyles = createStyles(({ token, css }) => ({ color: ${token.colorTextSecondary}; font-size: 12px; `, + pinIcon: css` + color: ${token.colorTextTertiary}; + cursor: pointer; + font-size: 14px; + + &:hover { + color: ${token.colorWarning}; + } + `, + pinIconPinned: css` + color: ${token.colorWarning}; + `, })); const AppList: React.FC = () => { @@ -118,6 +148,18 @@ const AppList: React.FC = () => { setSearchText, setSelectedAppId, } = useAppStore(); + const { + pinnedApps, + appSortBy, + appSortOrder, + togglePinnedApp, + setAppSortBy, + setAppSortOrder, + } = useUIStore(); + + const currentPinnedApps = currentDomain + ? pinnedApps[currentDomain.id] || [] + : []; // Load apps from Kintone const handleLoadApps = async () => { @@ -132,11 +174,7 @@ const AppList: React.FC = () => { }); if (result.success) { - // Sort by appId descending (newest first) - const sortedApps = [...result.data].sort( - (a, b) => parseInt(b.appId, 10) - parseInt(a.appId, 10), - ); - setApps(sortedApps); + setApps(result.data); } else { setError(result.error || "加载应用失败"); } @@ -147,35 +185,107 @@ const AppList: React.FC = () => { } }; - // Filter apps by search text - const filteredApps = React.useMemo(() => { - if (!searchText) return apps; - const lowerSearch = searchText.toLowerCase(); - return apps.filter( - (app) => - app.name.toLowerCase().includes(lowerSearch) || - app.appId.includes(searchText) || - (app.code && app.code.toLowerCase().includes(lowerSearch)), - ); - }, [apps, searchText]); + // Sort apps + const sortApps = (appsToSort: typeof apps) => { + const sorted = [...appsToSort]; + sorted.sort((a, b) => { + let comparison = 0; + + switch (appSortBy) { + case "name": + comparison = a.name.localeCompare(b.name); + break; + case "appId": + comparison = parseInt(a.appId, 10) - parseInt(b.appId, 10); + break; + default: + comparison = parseInt(a.appId, 10) - parseInt(b.appId, 10); + } + + return appSortOrder === "asc" ? comparison : -comparison; + }); + return sorted; + }; + + // Filter and sort apps + const processedApps = React.useMemo(() => { + let filtered = apps; + + // Filter by search text + if (searchText) { + const lowerSearch = searchText.toLowerCase(); + filtered = apps.filter( + (app) => + app.name.toLowerCase().includes(lowerSearch) || + app.appId.includes(searchText) || + (app.code && app.code.toLowerCase().includes(lowerSearch)), + ); + } + + // Sort + return sortApps(filtered); + }, [apps, searchText, appSortBy, appSortOrder]); + + // Separate pinned and unpinned apps + const { pinnedAppsList, unpinnedAppsList } = React.useMemo(() => { + const pinned: typeof apps = []; + const unpinned: typeof apps = []; + + processedApps.forEach((app) => { + if (currentPinnedApps.includes(app.appId)) { + pinned.push(app); + } else { + unpinned.push(app); + } + }); + + return { pinnedAppsList: pinned, unpinnedAppsList: unpinned }; + }, [processedApps, currentPinnedApps]); + + // Final display list: pinned first, then unpinned + const displayApps = [...pinnedAppsList, ...unpinnedAppsList]; // Handle item click const handleItemClick = (app: AppDetail) => { setSelectedAppId(app.appId); }; + // Handle pin toggle + const handlePinToggle = (e: React.MouseEvent, appId: string) => { + e.stopPropagation(); + if (currentDomain) { + togglePinnedApp(currentDomain.id, appId); + } + }; + + // Toggle sort order + const toggleSortOrder = () => { + setAppSortOrder(appSortOrder === "asc" ? "desc" : "asc"); + }; + // Render list item const renderItem = (app: AppDetail) => { const isActive = selectedAppId === app.appId; + const isPinned = currentPinnedApps.includes(app.appId); return ( handleItemClick(app)} >
+ + handlePinToggle(e, app.appId)} + > + {isPinned ? : } + + - {app.name} + + {app.name} + ID: {app.appId} @@ -194,7 +304,7 @@ const AppList: React.FC = () => { return (
- {/* Header with search and load button */} + {/* Header with search, sort and load button */}
{ onChange={(e) => setSearchText(e.target.value)} allowClear disabled={apps.length === 0 && !searchText} + size="small" />
- + +