This commit is contained in:
2026-03-15 15:21:10 +08:00
parent 7b1fd11bf3
commit 345f0b344c
5 changed files with 162 additions and 126 deletions

View File

@@ -31,6 +31,8 @@ npm run test:coverage # 测试覆盖率
**重要**: 修改 `src/main/` 目录下的文件后,必须运行 `npm test` 确保测试通过。
**注意**: 仅修改 `src/renderer/`(前端/UI`src/preload/` 文件时,不需要运行测试,只需确保类型检查通过(`npx tsc --noEmit`)即可。
## 2. 项目架构
```

View File

@@ -8,20 +8,20 @@ import { useTranslation } from "react-i18next";
import {
Layout,
Typography,
theme,
ConfigProvider,
App as AntApp,
Space,
} from "antd";
import { Button, Tooltip, DropdownMenu } from "@lobehub/ui";
import { SettingOutlined } from "@ant-design/icons";
import { Button, Tooltip, Modal } from "@lobehub/ui";
import {
Github,
Cloud,
CloudUpload,
History,
PanelLeftClose,
PanelLeftOpen,
Settings as SettingsIcon,
} from "lucide-react";
import { createStyles } from "antd-style";
@@ -37,7 +37,7 @@ const { Header, Content, Sider } = Layout;
const { Title } = Typography;
// Domain section heights
const DOMAIN_SECTION_COLLAPSED = 76; // 增加高度,避免按钮覆盖文字
const DOMAIN_SECTION_COLLAPSED = 76; // 增加高度,避免按钮覆盖文字
const DOMAIN_SECTION_EXPANDED = 240;
const DEFAULT_SIDER_WIDTH = 320;
const MIN_SIDER_WIDTH = 280;
@@ -150,9 +150,6 @@ const useStyles = createStyles(({ token, css }) => ({
const App: React.FC = () => {
const { t } = useTranslation("common");
const { styles } = useStyles();
const {
token: { colorBgContainer },
} = theme.useToken();
const { currentDomain } = useDomainStore();
const {
@@ -221,13 +218,10 @@ const App: React.FC = () => {
<div
style={{ display: "flex", alignItems: "center", gap: 8 }}
>
<Cloud
size={24}
style={{ color: "#1890ff" }}
/>
<Cloud size={24} style={{ color: "#1890ff" }} />
<span className={styles.logoText}>Kintone Manager</span>
</div>
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
<Button
type="text"
icon={<PanelLeftClose size={16} />}
@@ -274,7 +268,7 @@ const App: React.FC = () => {
<Header className={styles.header}>
<div className={styles.headerLeft}>
{siderCollapsed && (
<Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}>
<Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}>
<Button
type="text"
icon={<PanelLeftOpen size={16} />}
@@ -296,29 +290,18 @@ const App: React.FC = () => {
onClick={() => setDeployDialogOpen(true)}
disabled={!currentDomain}
>
{t("deployFiles")}
{t("deployFiles")}
</Button>
<Button icon={<History size={16} />} disabled={!currentDomain}>
{t("versionHistory")}
{t("versionHistory")}
</Button>
<DropdownMenu
trigger={<Button icon={<SettingOutlined />} />}
items={[
{
key: "settings",
icon: <SettingOutlined />,
label: t("settings"),
onClick: () => setSettingsOpen(true),
},
{ type: "divider" },
{
key: "github",
icon: <Github size={16} />,
label: "GitHub",
onClick: () => window.open("https://github.com", "_blank"),
},
]}
/>
<Tooltip title={t("settings")}>
<Button
icon={<SettingsIcon size={16} />}
onClick={() => setSettingsOpen(true)}
/>
</Tooltip>
</Space>
</Header>
@@ -337,12 +320,17 @@ const App: React.FC = () => {
onClose={() => setDeployDialogOpen(false)}
/>
{/* Settings Panel */}
{settingsOpen && (
<div style={{ position: "fixed", top: 0, right: 0, bottom: 0, width: 400, background: colorBgContainer, boxShadow: "-2px 0 8px rgba(0,0,0,0.15)", zIndex: 1000, overflow: "auto" }}>
<Settings onClose={() => setSettingsOpen(false)} />
</div>
)}
{/* Settings Modal */}
<Modal
title={t("settings")}
open={settingsOpen}
onCancel={() => setSettingsOpen(false)}
footer={null}
width={480}
>
<Settings />
</Modal>
</Layout>
</AntApp>
</ConfigProvider>

View File

@@ -4,13 +4,9 @@
*/
import React from "react";
import { motion } from "motion/react";
import { useTranslation } from "react-i18next";
import {
Input,
Spin,
Typography,
Space,
} from "antd";
import { Input, Spin, Typography, Space } from "antd";
import { Button, Tooltip, Empty, Select } from "@lobehub/ui";
import {
RefreshCw,
@@ -65,11 +61,11 @@ const useStyles = createStyles(({ token, css }) => ({
align-items: center;
height: 300px;
`,
listItem: css`
listItemMotion: css`
cursor: pointer;
transition: background 0.2s;
padding: ${token.paddingSM}px ${token.paddingMD}px !important;
border-bottom: 1px solid ${token.colorBorderSecondary} !important;
position: relative;
&:hover {
background: ${token.colorBgTextHover};
@@ -81,13 +77,10 @@ const useStyles = createStyles(({ token, css }) => ({
`,
listItemPinned: css`
background: ${token.colorWarningBg} !important;
`,
appInfo: css`
display: flex;
align-items: center;
gap: ${token.paddingSM}px;
flex: 1;
min-width: 0;
&:hover {
background: ${token.colorWarningBgHover} !important;
}
`,
appName: css`
font-weight: 500;
@@ -114,20 +107,124 @@ const useStyles = createStyles(({ token, css }) => ({
color: ${token.colorTextSecondary};
font-size: 12px;
`,
pinIcon: css`
appInfoWrapper: css`
display: flex;
align-items: center;
gap: ${token.paddingSM}px;
flex: 1;
min-width: 0;
position: relative;
`,
iconWrapper: css`
position: relative;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
`,
pinOverlay: css`
position: absolute;
top: 0;
left: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.15s ease;
z-index: 1;
`,
pinOverlayVisible: css`
opacity: 1;
`,
pinButton: css`
color: ${token.colorTextTertiary};
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: ${token.colorWarning};
}
`,
pinIconPinned: css`
pinButtonPinned: css`
color: ${token.colorWarning};
`,
}));
interface AppListItemProps {
app: AppDetail;
isActive: boolean;
isPinned: boolean;
onItemClick: (app: AppDetail) => void;
onPinToggle: (e: React.MouseEvent, appId: string) => void;
styles: ReturnType<typeof useStyles>["styles"];
t: (key: string) => string;
}
const AppListItem: React.FC<AppListItemProps> = ({
app,
isActive,
isPinned,
onItemClick,
onPinToggle,
styles,
t,
}) => {
const [isHovered, setIsHovered] = React.useState(false);
// Pin overlay is visible when:
// 1. Item is pinned (always show)
// 2. Item is hovered (show for unpinned items)
const showPinOverlay = isPinned || isHovered;
return (
<motion.div
layout
className={`${styles.listItemMotion} ${isActive ? styles.listItemActive : ""} ${isPinned ? styles.listItemPinned : ""}`}
onClick={() => onItemClick(app)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
<div className={styles.appInfoWrapper}>
<div className={styles.iconWrapper}>
{/* Pin overlay - visible on hover for unpinned, always visible for pinned */}
<div
className={`${styles.pinOverlay} ${showPinOverlay ? styles.pinOverlayVisible : ""}`}
onClick={(e) => onPinToggle(e, app.appId)}
>
<Tooltip title={isPinned ? t("unpin") : t("pinApp")}>
<span
className={`${styles.pinButton} ${isPinned ? styles.pinButtonPinned : ""}`}
>
{isPinned ? (
<Pin size={16} className="fill-current" />
) : (
<Pin size={16} />
)}
</span>
</Tooltip>
</div>
{/* App icon - hidden when pin overlay is visible */}
{!showPinOverlay && <LayoutGrid size={16} style={{ color: "#1890ff" }} />}
</div>
<Tooltip title={app.name}>
<span className={styles.appName}>{app.name}</span>
</Tooltip>
<Text code className={styles.appId}>
ID: {app.appId}
</Text>
</div>
</motion.div>
);
};
const AppList: React.FC = () => {
const { t } = useTranslation("app");
const { styles } = useStyles();
@@ -260,37 +357,6 @@ const AppList: React.FC = () => {
setAppSortOrder(appSortOrder === "asc" ? "desc" : "asc");
};
// Render list item
const renderItem = (app: AppDetail) => {
const isActive = selectedAppId === app.appId;
const isPinned = currentPinnedApps.includes(app.appId);
return (
<div
className={`${styles.listItem} ${isActive ? styles.listItemActive : ""} ${isPinned ? styles.listItemPinned : ""}`}
onClick={() => handleItemClick(app)}
>
<div className={styles.appInfo}>
<Tooltip title={isPinned ? t("unpin") : t("pinApp")}>
<span
className={`${styles.pinIcon} ${isPinned ? styles.pinIconPinned : ""}`}
onClick={(e) => handlePinToggle(e, app.appId)}
>
{isPinned ? <Pin size={16} className="fill-current" /> : <Pin size={16} />}
</span>
</Tooltip>
<LayoutGrid size={16} style={{ color: "#1890ff" }} />
<Tooltip title={app.name}>
<span className={styles.appName}>{app.name}</span>
</Tooltip>
<Text code className={styles.appId}>
ID: {app.appId}
</Text>
</div>
</div>
);
};
if (!currentDomain) {
return (
<div className={styles.empty}>
@@ -371,13 +437,18 @@ const AppList: React.FC = () => {
</Empty>
</div>
) : (
<>
{displayApps.map((app) => (
<React.Fragment key={app.appId}>
{renderItem(app)}
</React.Fragment>
))}
</>
displayApps.map((app) => (
<AppListItem
key={app.appId}
app={app}
isActive={selectedAppId === app.appId}
isPinned={currentPinnedApps.includes(app.appId)}
onItemClick={handleItemClick}
onPinToggle={handlePinToggle}
styles={styles}
t={t}
/>
))
)}
</div>
{/* Footer with info */}

View File

@@ -182,7 +182,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
<div className={styles.collapsedInfo}>
<Avatar
icon={<Cloud size={14} />}
size="small"
size={24}
style={{
backgroundColor: getDomainIconColor(currentDomain?.id),
}}

View File

@@ -1,18 +1,18 @@
/**
* Settings Component
* Application settings page with language switcher
* Application settings modal with language switcher
*/
import React from "react";
import { useTranslation } from "react-i18next";
import { Typography, Radio, Divider } from "antd";
import { Button } from "@lobehub/ui";
import { Globe, XCircle } from "lucide-react";
import { Globe } from "lucide-react";
import { createStyles } from "antd-style";
import { useLocaleStore } from "@renderer/stores/localeStore";
import { LOCALES, type LocaleCode } from "@shared/types/locale";
const { Title, Text } = Typography;
const { Title } = Typography;
const useStyles = createStyles(({ token, css }) => ({
container: css`
@@ -67,11 +67,7 @@ const useStyles = createStyles(({ token, css }) => ({
`,
}));
interface SettingsProps {
onClose?: () => void;
}
const Settings: React.FC<SettingsProps> = ({ onClose }) => {
const Settings: React.FC = () => {
const { t } = useTranslation("settings");
const { styles } = useStyles();
const { locale, setLocale } = useLocaleStore();
@@ -86,27 +82,6 @@ const Settings: React.FC<SettingsProps> = ({ onClose }) => {
return (
<div className={styles.container}>
{/* Header with close button */}
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 24,
}}
>
<Title level={4} style={{ margin: 0 }}>
{t("title")}
</Title>
{onClose && (
<Button
type="text"
icon={<XCircle size={16} />}
onClick={onClose}
/>
)}
</div>
{/* Language Section */}
<div className={styles.section}>
<div className={styles.sectionTitle}>