This commit is contained in:
2026-03-17 13:38:24 +08:00
parent 95acfd2b3b
commit dd5f16ef65
7 changed files with 153 additions and 144 deletions

View File

@@ -53,12 +53,21 @@ src/
│ ├── main.tsx # React 入口 │ ├── main.tsx # React 入口
│ ├── App.tsx # 根组件 │ ├── App.tsx # 根组件
│ ├── components/ # React 组件 │ ├── components/ # React 组件
── stores/ # Zustand Stores ── stores/ # Zustand Stores
│ └── locales/ # i18n 翻译文件
└── tests/ # 测试配置 └── tests/ # 测试配置
├── setup.ts # 测试环境设置 ├── setup.ts # 测试环境设置
└── mocks/ # Mock 文件 └── mocks/ # Mock 文件
``` ```
### 数据流
```
Renderer (React) → Preload API → IPC → Main Process → Storage/Kintone API
Result<T> 返回
```
## 3. 路径别名 ## 3. 路径别名
| 别名 | 路径 | | 别名 | 路径 |
@@ -123,6 +132,7 @@ type Result<T> = { success: true; data: T } | { success: false; error: string };
- IPC 调用使用 `invoke` 返回 `Result<T>` - IPC 调用使用 `invoke` 返回 `Result<T>`
- Preload 通过 `contextBridge.exposeInMainWorld` 暴露 API - Preload 通过 `contextBridge.exposeInMainWorld` 暴露 API
- 所有 IPC handlers 集中在 `src/main/ipc-handlers.ts`
## 6. UI 组件规范 ## 6. UI 组件规范
@@ -138,20 +148,27 @@ export const useStyles = createStyles(({ token, css }) => ({
})); }));
``` ```
- 国际化:使用日文默认 `import jaJP from 'antd/locale/ja_JP'`
- 禁止使用 Tailwind - 禁止使用 Tailwind
- **ESM Only**: LobeHub UI 仅支持 ESM
## 7. 安全规范 ## 7. 国际化 (i18n)
- 支持语言: `en-US`, `ja-JP`, `zh-CN`
- 翻译文件位置: `src/renderer/src/locales/{locale}/{namespace}.json`
- 使用 `react-i18next` 进行翻译
- Ant Design 默认使用日文: `import jaJP from 'antd/locale/ja_JP'`
## 8. 安全规范
- 密码使用 `electron``safeStorage` 加密存储 - 密码使用 `electron``safeStorage` 加密存储
- WebPreferences 必须:`contextIsolation: true`, `nodeIntegration: false`, `sandbox: false` - WebPreferences 必须:`contextIsolation: true`, `nodeIntegration: false`, `sandbox: false`
## 8. 错误处理 ## 9. 错误处理
- 所有 IPC 返回 `Result<T>` 格式 - 所有 IPC 返回 `Result<T>` 格式
- 渲染进程检查 `result.success` 处理错误 - 渲染进程检查 `result.success` 处理错误
## 9. fnm 环境配置 ## 10. fnm 环境配置
所有 npm/npx 命令需加载 fnm 环境: 所有 npm/npx 命令需加载 fnm 环境:
@@ -163,20 +180,19 @@ export const useStyles = createStyles(({ token, css }) => ({
eval "$(fnm env --use-on-cd)" && npm run dev eval "$(fnm env --use-on-cd)" && npm run dev
``` ```
## 10. 注意事项 ## 11. 技术栈约束
1. **ESM Only**: LobeHub UI 仅支持 ESM 1. **React 19**: 使用 `@types/react@^19.0.0`
2. **React 19**: 使用 `@types/react@^19.0.0` 2. **CSS 方案**: 使用 `antd-style`,禁止 Tailwind
3. **CSS 方案**: 使用 `antd-style`,禁止 Tailwind 3. **禁止 `as any`**: 使用类型守卫或 `unknown`
4. **禁止 `as any`**: 使用类型守卫或 `unknown` 4. **函数组件优先**: 禁止 class 组件
5. **函数组件优先**: 禁止 class 组件
## 11. 沟通规范 ## 12. 沟通规范
1. **人设**: 在回答的末尾加上「🦐」,用于确认上下文是否被正确保留 1. **人设**: 在回答的末尾加上「🦐」,用于确认上下文是否被正确保留
2. **语言**: 使用中文进行回答 2. **语言**: 使用中文进行回答
## 12. MVP Phase - Breaking Changes ## 13. MVP Phase - Breaking Changes
**This is MVP phase - breaking changes are acceptable for better design.** However, you MUST: **This is MVP phase - breaking changes are acceptable for better design.** However, you MUST:
@@ -202,7 +218,7 @@ eval "$(fnm env --use-on-cd)" && npm run dev
4. If significant, ask user for confirmation before implementing 4. If significant, ask user for confirmation before implementing
5. Update related documentation after implementation 5. Update related documentation after implementation
## 13. 测试规范 ## 14. 测试规范
### 测试框架 ### 测试框架

View File

@@ -35,8 +35,8 @@ 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 = 68; // 增加高度,避免按钮覆盖文字
const DOMAIN_SECTION_EXPANDED = 240; const DOMAIN_SECTION_EXPANDED = 260;
const DEFAULT_SIDER_WIDTH = 320; const DEFAULT_SIDER_WIDTH = 320;
const MIN_SIDER_WIDTH = 280; const MIN_SIDER_WIDTH = 280;
const MAX_SIDER_WIDTH = 500; const MAX_SIDER_WIDTH = 500;
@@ -61,13 +61,12 @@ const useStyles = createStyles(({ token, css }) => ({
overflow: hidden; overflow: hidden;
`, `,
logo: css` logo: css`
height: 48px; height: 32px;
margin: 8px 16px; margin: ${token.paddingXS}px ${token.padding}px ${token.paddingXXS}px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
border-bottom: 1px solid ${token.colorBorderSecondary};
`, `,
logoText: css` logoText: css`
color: ${token.colorText}; color: ${token.colorText};
@@ -75,7 +74,7 @@ const useStyles = createStyles(({ token, css }) => ({
font-weight: 600; font-weight: 600;
`, `,
siderContent: css` siderContent: css`
height: calc(100vh - 64px); height: calc(100vh - 44px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`, `,
@@ -113,6 +112,7 @@ const useStyles = createStyles(({ token, css }) => ({
`, `,
domainSection: css` domainSection: css`
border-bottom: 1px solid ${token.colorBorderSecondary}; border-bottom: 1px solid ${token.colorBorderSecondary};
padding-bottom: ${token.paddingXS}px;
overflow: hidden; overflow: hidden;
transition: height 0.2s ease-in-out; transition: height 0.2s ease-in-out;
`, `,
@@ -216,7 +216,7 @@ const App: React.FC = () => {
style={{ display: "flex", alignItems: "center", gap: 8 }} style={{ display: "flex", alignItems: "center", gap: 8 }}
> >
<Cloud size={24} style={{ color: token.colorPrimary }} /> <Cloud size={24} style={{ color: token.colorPrimary }} />
<span className={styles.logoText}>Kintone Manager</span> <span className={styles.logoText}>Kintone JS/CSS Manager</span>
</div> </div>
<Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}> <Tooltip title={t("collapseSidebar")} mouseEnterDelay={0.5}>
<Button <Button

View File

@@ -62,7 +62,7 @@ const useStyles = createStyles(({ token, css }) => ({
actions: css` actions: css`
position: absolute; position: absolute;
right: ${token.paddingXS}px; right: ${token.paddingXXS}px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
display: flex; display: flex;

View File

@@ -7,15 +7,19 @@
import React from "react"; import React from "react";
import { Spin } from "antd"; import { Spin } from "antd";
import { Button, Tooltip, Avatar, Empty } from "@lobehub/ui"; import { Button, Tooltip, Avatar, Empty, Block } from "@lobehub/ui";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Plus, Cloud, ChevronUp, ChevronDown } from "lucide-react"; import { Plus, Building, ChevronUp, ChevronDown } from "lucide-react";
import { createStyles } from "antd-style"; import { createStyles } from "antd-style";
import { useDomainStore, useUIStore } from "@renderer/stores"; import { useDomainStore } from "@renderer/stores";
import DomainList from "./DomainList"; import DomainList from "./DomainList";
import DomainForm from "./DomainForm"; import DomainForm from "./DomainForm";
const useStyles = createStyles(({ token, css }) => ({ const useStyles = createStyles(({ token, css }) => ({
wrapper: css`
height: 100%;
margin: 0 ${token.paddingSM}px;
`,
container: css` container: css`
height: 100%; height: 100%;
display: flex; display: flex;
@@ -24,37 +28,34 @@ const useStyles = createStyles(({ token, css }) => ({
border-radius: ${token.borderRadiusLG}px; border-radius: ${token.borderRadiusLG}px;
padding: ${token.paddingSM}px; padding: ${token.paddingSM}px;
`, `,
collapsedContainer: css`
height: 100%;
display: flex;
flex-direction: column;
background: ${token.colorFillSecondary};
border-radius: ${token.borderRadiusLG}px;
padding: ${token.paddingSM}px;
position: relative;
`,
header: css` header: css`
display: flex;
align-items: center;
padding: ${token.paddingXXS}px ${token.paddingSM}px;
`,
headerLeft: css`
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: ${token.marginXXS}px; flex: 1;
padding: 0 ${token.paddingLG}px; cursor: pointer;
padding-top: ${token.paddingXS}px; padding-right: ${token.paddingSM}px;
`,
headerRight: css`
display: flex;
align-items: center;
gap: ${token.paddingXS}px;
`, `,
title: css` title: css`
font-size: ${token.fontSizeHeading4}px; font-size: ${token.fontSize}px;
font-weight: ${token.fontWeightStrong}; font-weight: ${token.fontWeightStrong};
color: ${token.colorText}; color: ${token.colorText};
margin: 0; margin: 0;
`, `,
actions: css`
display: flex;
gap: ${token.paddingXS}px;
`,
content: css` content: css`
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 0 ${token.paddingSM}px; padding: 0 ${token.paddingXXS}px;
`, `,
loading: css` loading: css`
display: flex; display: flex;
@@ -62,21 +63,6 @@ const useStyles = createStyles(({ token, css }) => ({
align-items: center; align-items: center;
height: 200px; height: 200px;
`, `,
collapsedHeader: css`
display: flex;
justify-content: space-between;
align-items: center;
padding: ${token.paddingSM}px ${token.paddingMD}px;
cursor: pointer;
transition: background 0.2s;
`,
collapsedInfo: css`
display: flex;
align-items: center;
gap: ${token.paddingSM}px;
flex: 1;
min-width: 0;
`,
collapsedName: css` collapsedName: css`
font-weight: ${token.fontWeightStrong}; font-weight: ${token.fontWeightStrong};
font-size: ${token.fontSize}px; font-size: ${token.fontSize}px;
@@ -84,21 +70,38 @@ const useStyles = createStyles(({ token, css }) => ({
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
`, `,
collapsedUrl: css` collapsedDesc: css`
color: ${token.colorTextSecondary}; color: ${token.colorTextSecondary};
font-size: ${token.fontSizeSM}px; font-size: ${token.fontSizeSM}px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
`, `,
collapsedActions: css`
display: flex;
gap: ${token.paddingXS}px;
`,
noDomainText: css` noDomainText: css`
color: ${token.colorTextSecondary}; color: ${token.colorTextSecondary};
font-size: ${token.fontSizeSM}px; font-size: ${token.fontSizeSM}px;
padding: ${token.paddingSM}px ${token.paddingMD}px; `,
collapsedBlock: css`
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-radius: 8px;
`,
collapsedInfo: css`
display: flex;
align-items: center;
gap: 12px;
flex: 1;
min-width: 0;
`,
collapsedText: css`
flex: 1;
min-width: 0;
overflow: hidden;
`,
collapsedIcon: css`
flex-shrink: 0;
`, `,
})); }));
@@ -114,7 +117,6 @@ const DomainManager: React.FC<DomainManagerProps> = ({
const { t } = useTranslation("domain"); const { t } = useTranslation("domain");
const { styles } = useStyles(); const { styles } = useStyles();
const { domains, loading, loadDomains, currentDomain } = useDomainStore(); const { domains, loading, loadDomains, currentDomain } = useDomainStore();
const { domainIconColors } = useUIStore();
const [formOpen, setFormOpen] = React.useState(false); const [formOpen, setFormOpen] = React.useState(false);
const [editingDomain, setEditingDomain] = React.useState<string | null>(null); const [editingDomain, setEditingDomain] = React.useState<string | null>(null);
@@ -137,65 +139,53 @@ const DomainManager: React.FC<DomainManagerProps> = ({
setEditingDomain(null); setEditingDomain(null);
}; };
const getDomainIconColor = (domainId: string | undefined) => {
if (!domainId) return "#d9d9d9";
return domainIconColors[domainId] || "#1890ff";
};
// Collapsed view - show current domain only // Collapsed view - show current domain only
if (collapsed) { if (collapsed) {
return ( return (
<div className={styles.collapsedContainer}> <>
<div className={styles.collapsedHeader} onClick={onToggleCollapse}> <div className={styles.wrapper}>
<Block
direction="horizontal"
variant="filled"
clickable
onClick={onToggleCollapse}
className={styles.collapsedBlock}
>
<div className={styles.collapsedInfo}> <div className={styles.collapsedInfo}>
<Avatar <Avatar
icon={<Cloud size={14} />} size={36}
size={24} className={styles.collapsedIcon}
style={{ icon={<Building size={18} />}
backgroundColor: getDomainIconColor(currentDomain?.id),
}}
/> />
<div style={{ flex: 1, minWidth: 0 }}> <div className={styles.collapsedText}>
{currentDomain ? ( {currentDomain ? (
<> <>
<div className={styles.collapsedName}> <div className={styles.collapsedName}>
{currentDomain.name} {currentDomain.name}
</div> </div>
<div className={styles.collapsedUrl}> <div className={styles.collapsedDesc}>
{currentDomain.username} · {currentDomain.domain} {currentDomain.username} · <a target="_blank" href={"https://" + currentDomain.domain}>{currentDomain.domain}</a>
</div> </div>
</> </>
) : ( ) : (
<div className={styles.collapsedName}> <div className={styles.noDomainText}>
{t("noDomainSelected")} {t("noDomainSelected")}
</div> </div>
)} )}
</div> </div>
</div> </div>
<div className={styles.collapsedActions}>
<Tooltip title={t("expand")}> <Tooltip title={t("expand")}>
<Button <Button
type="text" type="text"
size="small" size="small"
icon={<ChevronDown size={16} />} icon={<ChevronDown size={18} />}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onToggleCollapse?.(); onToggleCollapse?.();
}} }}
/> />
</Tooltip> </Tooltip>
<Tooltip title={t("addDomain")}> </Block>
<Button
type="text"
size="small"
icon={<Plus size={16} />}
onClick={(e) => {
e.stopPropagation();
handleAdd();
}}
/>
</Tooltip>
</div>
</div> </div>
<DomainForm <DomainForm
@@ -203,29 +193,31 @@ const DomainManager: React.FC<DomainManagerProps> = ({
onClose={handleCloseForm} onClose={handleCloseForm}
domainId={editingDomain} domainId={editingDomain}
/> />
</div> </>
); );
} }
// Expanded view - full list // Expanded view - full list
return ( return (
<>
<div className={styles.wrapper}>
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<h2 className={styles.title}>{t("domainManagement")}</h2> <Tooltip title={t("collapse")} placement="topRight">
<div className={styles.actions}> <div className={styles.headerLeft} onClick={onToggleCollapse}>
<Tooltip title={t("collapse")}> <h3 className={styles.title}>{t("domainManagement")}</h3>
<Button <ChevronUp size={16} style={{ opacity: 0.5 }} />
type="text" </div>
size="small"
icon={<ChevronUp size={16} />}
onClick={onToggleCollapse}
/>
</Tooltip> </Tooltip>
<div className={styles.headerRight}>
<Tooltip title={t("addDomain")}>
<Button <Button
type="primary" type="primary"
size="small"
icon={<Plus size={16} />} icon={<Plus size={16} />}
onClick={handleAdd} onClick={handleAdd}
></Button> />
</Tooltip>
</div> </div>
</div> </div>
@@ -240,13 +232,14 @@ const DomainManager: React.FC<DomainManagerProps> = ({
<DomainList onEdit={handleEdit} /> <DomainList onEdit={handleEdit} />
)} )}
</div> </div>
</div>
</div>
<DomainForm <DomainForm
open={formOpen} open={formOpen}
onClose={handleCloseForm} onClose={handleCloseForm}
domainId={editingDomain} domainId={editingDomain}
/> />
</div> </>
); );
}; };

View File

@@ -1,5 +1,5 @@
{ {
"appName": "Kintone Manager", "appName": "Kintone JS/CSS Manager",
"save": "Save", "save": "Save",
"cancel": "Cancel", "cancel": "Cancel",
"delete": "Delete", "delete": "Delete",

View File

@@ -1,5 +1,5 @@
{ {
"appName": "Kintone Manager", "appName": "Kintone JS/CSS Manager",
"save": "保存", "save": "保存",
"cancel": "キャンセル", "cancel": "キャンセル",
"delete": "削除", "delete": "削除",

View File

@@ -1,5 +1,5 @@
{ {
"appName": "Kintone Manager", "appName": "Kintone JS/CSS Manager",
"save": "保存", "save": "保存",
"cancel": "取消", "cancel": "取消",
"delete": "删除", "delete": "删除",