From b34720fccf5291406f3f958f6450e5257f2878b5 Mon Sep 17 00:00:00 2001 From: xue jiahao Date: Mon, 16 Mar 2026 13:42:29 +0800 Subject: [PATCH] fix UI --- .vscode/settings.json | 11 + MEMORY.md | 42 +++ src/main/index.ts | 7 + .../src/components/AppDetail/AppDetail.tsx | 6 +- .../src/components/AppList/AppList.tsx | 1 - .../src/components/CodeViewer/CodeViewer.tsx | 1 - .../components/DomainManager/DomainForm.tsx | 344 +++++++++++++----- .../components/DomainManager/DomainList.tsx | 274 +++++--------- .../DomainManager/DomainManager.tsx | 13 +- .../VersionHistory/VersionHistory.tsx | 2 - src/renderer/src/locales/en-US/domain.json | 3 +- src/renderer/src/locales/ja-JP/domain.json | 3 +- src/renderer/src/locales/zh-CN/domain.json | 3 +- 13 files changed, 428 insertions(+), 282 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 MEMORY.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3b97e6e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "i18n-ally.localesPaths": [ + "src/renderer/src/locales" + ], + "i18n-ally.keystyle": "nested", + "i18n-ally.namespace": true, + "i18n-ally.sourceLanguage": "zh-CN", + "i18n-ally.enabledParsers": [ + "json" + ], +} \ No newline at end of file diff --git a/MEMORY.md b/MEMORY.md new file mode 100644 index 0000000..7ab9711 --- /dev/null +++ b/MEMORY.md @@ -0,0 +1,42 @@ +# MEMORY.md + +## 2026-03-15 - CSS 模板字符串语法错误 + +### 遇到什么问题 + +- 在使用 `edit` 工具修改 `DomainForm.tsx` 中的 CSS 样式时,只替换了部分内容,导致 CSS 模板字符串语法错误 +- 错误信息:`Unexpected token, expected ","` 在 `passwordHint` 定义处 +- 原因:`.ant-form-item` 的 CSS 块没有正确关闭,缺少 `}` 和模板字符串结束符 `` ` `` + +### 如何解决的 + +- 使用 `edit` 工具完整替换整个 `useStyles` 定义块,确保所有 CSS 模板字符串正确关闭 + +### 以后如何避免 + +- 修改 CSS-in-JS 样式时,尽量替换完整的样式块而非单行 +- 修改后立即运行 `npx tsc --noEmit` 验证语法 +- 注意模板字符串的开始 `` ` `` 和结束 `` ` `` 必须成对出现 + +--- + +## 2026-03-15 - UI 重构经验 + +### 变更内容 + +1. **DomainForm 表单间距**:`marginMD` → `marginSM` +2. **AppDetail 头部布局**:标题和按钮同一行(flex 布局) +3. **AppDetail Tabs 重构**: + - 移除 Tabs 组件 + - 移除 "基本信息" tab + - 合并 4 个 JS/CSS tab 为单页面(选项 A:单列滚动列表 + 分区标题) + - 新增 `viewMode` 状态管理列表/代码视图切换 + - 点击文件进入代码视图,带返回按钮 + +### 文件修改 + +- `src/renderer/src/components/DomainManager/DomainForm.tsx` +- `src/renderer/src/components/AppDetail/AppDetail.tsx` +- `src/renderer/src/locales/zh-CN/app.json` - 添加 `backToList` +- `src/renderer/src/locales/en-US/app.json` - 添加 `backToList` +- `src/renderer/src/locales/ja-JP/app.json` - 添加 `backToList` diff --git a/src/main/index.ts b/src/main/index.ts index 35cf0ae..4de08ea 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -31,6 +31,13 @@ function createWindow(): void { mainWindow.show(); }); + // 阻止 Alt 键显示默认菜单栏 + mainWindow.webContents.on("before-input-event", (event, input) => { + if (input.key === "Alt" && input.type === "keyDown") { + event.preventDefault(); + } + }); + mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url); return { action: "deny" }; diff --git a/src/renderer/src/components/AppDetail/AppDetail.tsx b/src/renderer/src/components/AppDetail/AppDetail.tsx index a19c5c7..2ee3296 100644 --- a/src/renderer/src/components/AppDetail/AppDetail.tsx +++ b/src/renderer/src/components/AppDetail/AppDetail.tsx @@ -105,9 +105,13 @@ const useStyles = createStyles(({ token, css }) => ({ white-space: nowrap; `, emptySection: css` + height: 100%; padding: ${token.paddingLG}px; text-align: center; color: ${token.colorTextSecondary}; + display: flex; + justify-content: center; + align-items: center; `, sectionHeader: css` display: flex; @@ -199,7 +203,6 @@ const AppDetail: React.FC = () => {
@@ -220,7 +223,6 @@ const AppDetail: React.FC = () => {
diff --git a/src/renderer/src/components/AppList/AppList.tsx b/src/renderer/src/components/AppList/AppList.tsx index 17942aa..39d2b24 100644 --- a/src/renderer/src/components/AppList/AppList.tsx +++ b/src/renderer/src/components/AppList/AppList.tsx @@ -429,7 +429,6 @@ const AppList: React.FC = () => {
+ ); }; return ( @@ -167,7 +317,7 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { destroyOnHidden mask={{ closable: false }} > -
+ @@ -180,17 +330,8 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { { validator: (_, value) => { if (!value) return Promise.resolve(); - // Allow https:// or http:// prefix - let domain = value.trim(); - if (domain.startsWith("https://")) { - domain = domain.slice(8); - } else if (domain.startsWith("http://")) { - domain = domain.slice(7); - } - // Remove trailing slashes - domain = domain.replace(/\/+$/, ""); - // Validate domain format - if (/^[\w.-]+$/.test(domain)) { + const processed = processDomain(value); + if (validateDomainFormat(processed)) { return Promise.resolve(); } return Promise.reject(new Error(t("validDomainRequired"))); @@ -212,21 +353,42 @@ const DomainForm: React.FC = ({ open, onClose, domainId }) => { - - -
- -
+ +
+ {/* Left side: Cancel button and error message */} +
-
+ + {/* Right side: Test button and Create/Update button */} +
+ {createError && ( + + {createError.message} + + )} + {renderTestButton()} +
diff --git a/src/renderer/src/components/DomainManager/DomainList.tsx b/src/renderer/src/components/DomainManager/DomainList.tsx index b254551..343e1f3 100644 --- a/src/renderer/src/components/DomainManager/DomainList.tsx +++ b/src/renderer/src/components/DomainManager/DomainList.tsx @@ -1,33 +1,23 @@ /** * DomainList Component - * Displays list of domains with connection status and drag-to-reorder + * Displays list of domains with drag-to-reorder functionality */ - import React from "react"; import { useTranslation } from "react-i18next"; -import { Tag, Popconfirm, Space } from "antd"; -import { Button, Tooltip } from "@lobehub/ui"; -import { Avatar } from "@lobehub/ui"; -import { - Cloud, - Pencil, - Trash2, - CheckCircle, - XCircle, - HelpCircle, - GripVertical, -} from "lucide-react"; +import { Popconfirm, Space } from "antd"; +import { Button, SortableList, Tooltip } from "@lobehub/ui"; +import { Pencil, Trash2 } from "lucide-react"; import { createStyles } from "antd-style"; -import { useDomainStore, useUIStore } from "@renderer/stores"; +import { useDomainStore } from "@renderer/stores"; import type { Domain } from "@shared/types/domain"; const useStyles = createStyles(({ token, css }) => ({ - item: css` - padding: ${token.paddingSM}px; + itemWrapper: css` + width: 100%; + padding: ${token.paddingXS}px ${token.paddingSM}px; border-radius: ${token.borderRadiusLG}px; background: ${token.colorBgContainer}; border: 1px solid ${token.colorBorderSecondary}; - margin-bottom: ${token.marginXS}px; transition: all 0.2s; cursor: pointer; @@ -35,15 +25,17 @@ const useStyles = createStyles(({ token, css }) => ({ border-color: ${token.colorPrimary}; box-shadow: ${token.boxShadowSecondary}; } + + &:hover .domain-item-actions { + opacity: 1; + } `, + selectedItem: css` border-color: ${token.colorPrimary}; background: ${token.colorPrimaryBg}; `, - itemDragging: css` - opacity: 0.5; - border: 2px dashed ${token.colorPrimary}; - `, + domainInfo: css` display: flex; align-items: center; @@ -51,6 +43,7 @@ const useStyles = createStyles(({ token, css }) => ({ flex: 1; min-width: 0; `, + domainName: css` font-weight: ${token.fontWeightStrong}; font-size: ${token.fontSizeLG}px; @@ -58,6 +51,7 @@ const useStyles = createStyles(({ token, css }) => ({ text-overflow: ellipsis; white-space: nowrap; `, + domainUrl: css` color: ${token.colorTextSecondary}; font-size: ${token.fontSizeSM}px; @@ -65,33 +59,29 @@ const useStyles = createStyles(({ token, css }) => ({ text-overflow: ellipsis; white-space: nowrap; `, + actions: css` + position: absolute; + right: ${token.paddingXS}px; + top: 50%; + transform: translateY(-50%); display: flex; gap: ${token.paddingXS}px; + opacity: 0; + transition: opacity 0.2s; + background: ${token.colorBgContainer}; + border-radius: ${token.borderRadiusSM}px; + padding: 2px; + box-shadow: ${token.boxShadowSecondary}; `, - statusTag: css` - margin-left: ${token.paddingSM}px; - `, - dragHandle: css` - cursor: grab; - color: ${token.colorTextTertiary}; - display: flex; - align-items: center; - &:hover { - color: ${token.colorText}; - } - - &:active { - cursor: grabbing; - } - `, itemContent: css` display: flex; - justify-content: space-between; align-items: center; - gap: ${token.paddingSM}px; + gap: ${token.paddingXS}px; + position: relative; `, + domainText: css` flex: 1; min-width: 0; @@ -106,17 +96,8 @@ interface DomainListProps { const DomainList: React.FC = ({ onEdit }) => { const { t } = useTranslation("domain"); const { styles } = useStyles(); - const { - domains, - currentDomain, - connectionStatuses, - switchDomain, - deleteDomain, - testConnection, - reorderDomains, - } = useDomainStore(); - const { domainIconColors } = useUIStore(); - const [draggingIndex, setDraggingIndex] = React.useState(null); + const { domains, currentDomain, switchDomain, deleteDomain, reorderDomains } = + useDomainStore(); const handleSelect = (domain: Domain) => { switchDomain(domain); @@ -126,131 +107,69 @@ const DomainList: React.FC = ({ onEdit }) => { await deleteDomain(id); }; - const getStatusIcon = (id: string) => { - const status = connectionStatuses[id]; - switch (status) { - case "connected": - return ; - case "error": - return ; - default: - return ; + // Handle reorder - convert SortableListItem[] back to reorder action + const handleSortChange = (newItems: { id: string }[]) => { + const newOrder = newItems.map((item) => item.id); + const oldOrder = domains.map((d) => d.id); + + // Find the from and to indices + for (let i = 0; i < oldOrder.length; i++) { + if (oldOrder[i] !== newOrder[i]) { + const fromIndex = i; + const toIndex = newOrder.indexOf(oldOrder[i]); + reorderDomains(fromIndex, toIndex); + break; + } } }; - const getStatusTag = (id: string) => { - const status = connectionStatuses[id]; - switch (status) { - case "connected": - return {t("connected")}; - case "error": - return {t("connectionFailed")}; - default: - return {t("notTested")}; - } - }; + const renderItem = (item: { id: string }) => { + const domain = domains.find((d) => d.id === item.id); + if (!domain) return null; - const getDomainIconColor = (domainId: string, isSelected: boolean) => { - if (domainIconColors[domainId]) { - return domainIconColors[domainId]; - } - return isSelected ? "#1890ff" : "#87d068"; - }; + const isSelected = currentDomain?.id === domain.id; - // Drag and drop handlers - const handleDragStart = (index: number) => { - setDraggingIndex(index); - }; - - const handleDragOver = (e: React.DragEvent, index: number) => { - e.preventDefault(); - if (draggingIndex === null || draggingIndex === index) return; - }; - - const handleDrop = (e: React.DragEvent, index: number) => { - e.preventDefault(); - if (draggingIndex === null || draggingIndex === index) return; - reorderDomains(draggingIndex, index); - setDraggingIndex(null); - }; - - const handleDragEnd = () => { - setDraggingIndex(null); - }; - - return ( - <> - {domains.map((domain, index) => { - const isSelected = currentDomain?.id === domain.id; - const isDragging = draggingIndex === index; - - return ( -
handleSelect(domain)} - draggable - onDragStart={() => handleDragStart(index)} - onDragOver={(e) => handleDragOver(e, index)} - onDrop={(e) => handleDrop(e, index)} - onDragEnd={handleDragEnd} - > -
-
- -
-
- } - style={{ - backgroundColor: getDomainIconColor(domain.id, isSelected), - }} - /> -
-
- {domain.name} - {getStatusTag(domain.id)} -
-
- {domain.domain} · {domain.username} -
+ return ( + +
handleSelect(domain)} + > +
+ +
+
+
{domain.name}
+
+ {domain.username} · {domain.domain}
+
- - -
+ + +
- ); - })} - +
+ + ); + }; + + return ( + ); }; diff --git a/src/renderer/src/components/DomainManager/DomainManager.tsx b/src/renderer/src/components/DomainManager/DomainManager.tsx index 0d28593..1295049 100644 --- a/src/renderer/src/components/DomainManager/DomainManager.tsx +++ b/src/renderer/src/components/DomainManager/DomainManager.tsx @@ -38,10 +38,9 @@ const useStyles = createStyles(({ token, css }) => ({ display: flex; justify-content: space-between; align-items: center; - margin-bottom: ${token.marginLG}px; + margin-bottom: ${token.marginXXS}px; padding: 0 ${token.paddingLG}px; - padding-top: ${token.paddingLG}px; - border-bottom: 1px solid ${token.colorBorderSecondary}; + padding-top: ${token.paddingXS}px; `, title: css` font-size: ${token.fontSizeHeading4}px; @@ -56,7 +55,7 @@ const useStyles = createStyles(({ token, css }) => ({ content: css` flex: 1; overflow: auto; - padding: 0 ${token.paddingLG}px; + padding: 0 ${token.paddingSM}px; `, loading: css` display: flex; @@ -194,7 +193,7 @@ const DomainManager: React.FC = ({ {currentDomain.name}
- {currentDomain.domain} + {currentDomain.username} · {currentDomain.domain}
) : ( @@ -256,11 +255,7 @@ const DomainManager: React.FC = ({ ) : domains.length === 0 ? ( - ) : ( diff --git a/src/renderer/src/components/VersionHistory/VersionHistory.tsx b/src/renderer/src/components/VersionHistory/VersionHistory.tsx index effff54..a6ba88a 100644 --- a/src/renderer/src/components/VersionHistory/VersionHistory.tsx +++ b/src/renderer/src/components/VersionHistory/VersionHistory.tsx @@ -194,7 +194,6 @@ const VersionHistory: React.FC = () => {
@@ -230,7 +229,6 @@ const VersionHistory: React.FC = () => { {versions.length === 0 ? ( ) : (