fix UI
This commit is contained in:
@@ -31,6 +31,8 @@ npm run test:coverage # 测试覆盖率
|
|||||||
|
|
||||||
**重要**: 修改 `src/main/` 目录下的文件后,必须运行 `npm test` 确保测试通过。
|
**重要**: 修改 `src/main/` 目录下的文件后,必须运行 `npm test` 确保测试通过。
|
||||||
|
|
||||||
|
**注意**: 仅修改 `src/renderer/`(前端/UI)或 `src/preload/` 文件时,不需要运行测试,只需确保类型检查通过(`npx tsc --noEmit`)即可。
|
||||||
|
|
||||||
## 2. 项目架构
|
## 2. 项目架构
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ import { useTranslation } from "react-i18next";
|
|||||||
import {
|
import {
|
||||||
Layout,
|
Layout,
|
||||||
Typography,
|
Typography,
|
||||||
theme,
|
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
App as AntApp,
|
App as AntApp,
|
||||||
Space,
|
Space,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { Button, Tooltip, DropdownMenu } from "@lobehub/ui";
|
|
||||||
import { SettingOutlined } from "@ant-design/icons";
|
import { Button, Tooltip, Modal } from "@lobehub/ui";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Github,
|
|
||||||
Cloud,
|
Cloud,
|
||||||
CloudUpload,
|
CloudUpload,
|
||||||
History,
|
History,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
PanelLeftOpen,
|
PanelLeftOpen,
|
||||||
|
Settings as SettingsIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
@@ -37,7 +37,7 @@ const { Header, Content, Sider } = Layout;
|
|||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
// Domain section heights
|
// Domain section heights
|
||||||
const DOMAIN_SECTION_COLLAPSED = 76; // 增加高度,避免按钮覆盖文字
|
const DOMAIN_SECTION_COLLAPSED = 76; // 增加高度,避免按钮覆盖文字
|
||||||
const DOMAIN_SECTION_EXPANDED = 240;
|
const DOMAIN_SECTION_EXPANDED = 240;
|
||||||
const DEFAULT_SIDER_WIDTH = 320;
|
const DEFAULT_SIDER_WIDTH = 320;
|
||||||
const MIN_SIDER_WIDTH = 280;
|
const MIN_SIDER_WIDTH = 280;
|
||||||
@@ -150,9 +150,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const {
|
|
||||||
token: { colorBgContainer },
|
|
||||||
} = theme.useToken();
|
|
||||||
|
|
||||||
const { currentDomain } = useDomainStore();
|
const { currentDomain } = useDomainStore();
|
||||||
const {
|
const {
|
||||||
@@ -221,13 +218,10 @@ const App: React.FC = () => {
|
|||||||
<div
|
<div
|
||||||
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||||
>
|
>
|
||||||
<Cloud
|
<Cloud size={24} style={{ color: "#1890ff" }} />
|
||||||
size={24}
|
|
||||||
style={{ color: "#1890ff" }}
|
|
||||||
/>
|
|
||||||
<span className={styles.logoText}>Kintone Manager</span>
|
<span className={styles.logoText}>Kintone Manager</span>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
|
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<PanelLeftClose size={16} />}
|
icon={<PanelLeftClose size={16} />}
|
||||||
@@ -274,7 +268,7 @@ const App: React.FC = () => {
|
|||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<div className={styles.headerLeft}>
|
<div className={styles.headerLeft}>
|
||||||
{siderCollapsed && (
|
{siderCollapsed && (
|
||||||
<Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}>
|
<Tooltip title={t("expandSidebar")} mouseEnterDelay={0.5}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<PanelLeftOpen size={16} />}
|
icon={<PanelLeftOpen size={16} />}
|
||||||
@@ -296,29 +290,18 @@ const App: React.FC = () => {
|
|||||||
onClick={() => setDeployDialogOpen(true)}
|
onClick={() => setDeployDialogOpen(true)}
|
||||||
disabled={!currentDomain}
|
disabled={!currentDomain}
|
||||||
>
|
>
|
||||||
{t("deployFiles")}
|
{t("deployFiles")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<History size={16} />} disabled={!currentDomain}>
|
<Button icon={<History size={16} />} disabled={!currentDomain}>
|
||||||
{t("versionHistory")}
|
{t("versionHistory")}
|
||||||
</Button>
|
</Button>
|
||||||
<DropdownMenu
|
<Tooltip title={t("settings")}>
|
||||||
trigger={<Button icon={<SettingOutlined />} />}
|
<Button
|
||||||
items={[
|
icon={<SettingsIcon size={16} />}
|
||||||
{
|
onClick={() => setSettingsOpen(true)}
|
||||||
key: "settings",
|
/>
|
||||||
icon: <SettingOutlined />,
|
</Tooltip>
|
||||||
label: t("settings"),
|
|
||||||
onClick: () => setSettingsOpen(true),
|
|
||||||
},
|
|
||||||
{ type: "divider" },
|
|
||||||
{
|
|
||||||
key: "github",
|
|
||||||
icon: <Github size={16} />,
|
|
||||||
label: "GitHub",
|
|
||||||
onClick: () => window.open("https://github.com", "_blank"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
@@ -337,12 +320,17 @@ const App: React.FC = () => {
|
|||||||
onClose={() => setDeployDialogOpen(false)}
|
onClose={() => setDeployDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Settings Panel */}
|
{/* Settings Modal */}
|
||||||
{settingsOpen && (
|
<Modal
|
||||||
<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" }}>
|
title={t("settings")}
|
||||||
<Settings onClose={() => setSettingsOpen(false)} />
|
open={settingsOpen}
|
||||||
</div>
|
onCancel={() => setSettingsOpen(false)}
|
||||||
)}
|
footer={null}
|
||||||
|
width={480}
|
||||||
|
>
|
||||||
|
<Settings />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</AntApp>
|
</AntApp>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import { Input, Spin, Typography, Space } from "antd";
|
||||||
Input,
|
|
||||||
Spin,
|
|
||||||
Typography,
|
|
||||||
Space,
|
|
||||||
} from "antd";
|
|
||||||
import { Button, Tooltip, Empty, Select } from "@lobehub/ui";
|
import { Button, Tooltip, Empty, Select } from "@lobehub/ui";
|
||||||
import {
|
import {
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -65,11 +61,11 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
`,
|
`,
|
||||||
listItem: css`
|
listItemMotion: css`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
|
||||||
padding: ${token.paddingSM}px ${token.paddingMD}px !important;
|
padding: ${token.paddingSM}px ${token.paddingMD}px !important;
|
||||||
border-bottom: 1px solid ${token.colorBorderSecondary} !important;
|
border-bottom: 1px solid ${token.colorBorderSecondary} !important;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: ${token.colorBgTextHover};
|
background: ${token.colorBgTextHover};
|
||||||
@@ -81,13 +77,10 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
`,
|
`,
|
||||||
listItemPinned: css`
|
listItemPinned: css`
|
||||||
background: ${token.colorWarningBg} !important;
|
background: ${token.colorWarningBg} !important;
|
||||||
`,
|
|
||||||
appInfo: css`
|
&:hover {
|
||||||
display: flex;
|
background: ${token.colorWarningBgHover} !important;
|
||||||
align-items: center;
|
}
|
||||||
gap: ${token.paddingSM}px;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
`,
|
`,
|
||||||
appName: css`
|
appName: css`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -114,20 +107,124 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
color: ${token.colorTextSecondary};
|
color: ${token.colorTextSecondary};
|
||||||
font-size: 12px;
|
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};
|
color: ${token.colorTextTertiary};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: ${token.colorWarning};
|
color: ${token.colorWarning};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
pinIconPinned: css`
|
pinButtonPinned: css`
|
||||||
color: ${token.colorWarning};
|
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 AppList: React.FC = () => {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
@@ -260,37 +357,6 @@ const AppList: React.FC = () => {
|
|||||||
setAppSortOrder(appSortOrder === "asc" ? "desc" : "asc");
|
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) {
|
if (!currentDomain) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.empty}>
|
<div className={styles.empty}>
|
||||||
@@ -371,13 +437,18 @@ const AppList: React.FC = () => {
|
|||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
displayApps.map((app) => (
|
||||||
{displayApps.map((app) => (
|
<AppListItem
|
||||||
<React.Fragment key={app.appId}>
|
key={app.appId}
|
||||||
{renderItem(app)}
|
app={app}
|
||||||
</React.Fragment>
|
isActive={selectedAppId === app.appId}
|
||||||
))}
|
isPinned={currentPinnedApps.includes(app.appId)}
|
||||||
</>
|
onItemClick={handleItemClick}
|
||||||
|
onPinToggle={handlePinToggle}
|
||||||
|
styles={styles}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Footer with info */}
|
{/* Footer with info */}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
<div className={styles.collapsedInfo}>
|
<div className={styles.collapsedInfo}>
|
||||||
<Avatar
|
<Avatar
|
||||||
icon={<Cloud size={14} />}
|
icon={<Cloud size={14} />}
|
||||||
size="small"
|
size={24}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: getDomainIconColor(currentDomain?.id),
|
backgroundColor: getDomainIconColor(currentDomain?.id),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Settings Component
|
* Settings Component
|
||||||
* Application settings page with language switcher
|
* Application settings modal with language switcher
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Typography, Radio, Divider } from "antd";
|
import { Typography, Radio, Divider } from "antd";
|
||||||
import { Button } from "@lobehub/ui";
|
import { Globe } from "lucide-react";
|
||||||
import { Globe, XCircle } from "lucide-react";
|
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import { useLocaleStore } from "@renderer/stores/localeStore";
|
import { useLocaleStore } from "@renderer/stores/localeStore";
|
||||||
import { LOCALES, type LocaleCode } from "@shared/types/locale";
|
import { LOCALES, type LocaleCode } from "@shared/types/locale";
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
container: css`
|
container: css`
|
||||||
@@ -67,11 +67,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface SettingsProps {
|
const Settings: React.FC = () => {
|
||||||
onClose?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Settings: React.FC<SettingsProps> = ({ onClose }) => {
|
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const { locale, setLocale } = useLocaleStore();
|
const { locale, setLocale } = useLocaleStore();
|
||||||
@@ -86,27 +82,6 @@ const Settings: React.FC<SettingsProps> = ({ onClose }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<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 */}
|
{/* Language Section */}
|
||||||
<div className={styles.section}>
|
<div className={styles.section}>
|
||||||
<div className={styles.sectionTitle}>
|
<div className={styles.sectionTitle}>
|
||||||
|
|||||||
Reference in New Issue
Block a user