ui fix
This commit is contained in:
44
AGENTS.md
44
AGENTS.md
@@ -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. 测试规范
|
||||
|
||||
### 测试框架
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"appName": "Kintone Manager",
|
||||
"appName": "Kintone JS/CSS Manager",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"appName": "Kintone Manager",
|
||||
"appName": "Kintone JS/CSS Manager",
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"appName": "Kintone Manager",
|
||||
"appName": "Kintone JS/CSS Manager",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
|
||||
Reference in New Issue
Block a user