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

View File

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

View File

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

View File

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

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": "保存",
"cancel": "キャンセル",
"delete": "削除",

View File

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