fix UI
This commit is contained in:
@@ -31,6 +31,8 @@ npm run test:coverage # 测试覆盖率
|
||||
|
||||
**重要**: 修改 `src/main/` 目录下的文件后,必须运行 `npm test` 确保测试通过。
|
||||
|
||||
**注意**: 仅修改 `src/renderer/`(前端/UI)或 `src/preload/` 文件时,不需要运行测试,只需确保类型检查通过(`npx tsc --noEmit`)即可。
|
||||
|
||||
## 2. 项目架构
|
||||
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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),
|
||||
}}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user