feat(ui): refactor AppDetail layout and add download with save dialog
- Remove Tabs component, merge JS/CSS sections into single scrollable list - Add sub-page navigation for code viewing with back button - Put app name and buttons on same row to save vertical space - Reduce DomainForm spacing (marginMD -> marginSM) - Use FileCode/FileText icons to indicate JS/CSS file types - Add save dialog for single file download with loading state - Add IPC handlers: showSaveDialog, saveFileContent - Fix duplicate file extension issue in download filenames - Add i18n keys: backToList, downloadSuccess, downloadFailed, downloadAllSuccess
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* Based on REQUIREMENTS.md:228-268
|
||||
*/
|
||||
|
||||
import { ipcMain } from "electron";
|
||||
import { ipcMain, dialog } from "electron";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
saveDomain,
|
||||
@@ -573,6 +573,39 @@ export function registerIpcHandlers(): void {
|
||||
registerGetLocale();
|
||||
registerSetLocale();
|
||||
|
||||
// Dialog
|
||||
registerShowSaveDialog();
|
||||
registerSaveFileContent();
|
||||
|
||||
console.log("IPC handlers registered");
|
||||
}
|
||||
|
||||
// ==================== Dialog IPC Handlers ====================
|
||||
|
||||
/**
|
||||
* Show save dialog
|
||||
*/
|
||||
function registerShowSaveDialog(): void {
|
||||
handle<{ defaultPath?: string }, string | null>("showSaveDialog", async (params) => {
|
||||
const result = await dialog.showSaveDialog({
|
||||
defaultPath: params.defaultPath,
|
||||
filters: [
|
||||
{ name: "JavaScript", extensions: ["js"] },
|
||||
{ name: "CSS", extensions: ["css"] },
|
||||
{ name: "All Files", extensions: ["*"] },
|
||||
],
|
||||
});
|
||||
return result.filePath || null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save file content to disk
|
||||
*/
|
||||
function registerSaveFileContent(): void {
|
||||
handle<{ filePath: string; content: string }, void>("saveFileContent", async (params) => {
|
||||
const fs = await import("fs");
|
||||
const buffer = Buffer.from(params.content, "base64");
|
||||
await fs.promises.writeFile(params.filePath, buffer);
|
||||
});
|
||||
}
|
||||
|
||||
5
src/preload/index.d.ts
vendored
5
src/preload/index.d.ts
vendored
@@ -14,6 +14,8 @@ import type {
|
||||
GetVersionsParams,
|
||||
RollbackParams,
|
||||
SetLocaleParams,
|
||||
ShowSaveDialogParams,
|
||||
SaveFileContentParams,
|
||||
} from "@shared/types/ipc";
|
||||
import type { Domain, DomainWithStatus } from "@shared/types/domain";
|
||||
import type {
|
||||
@@ -68,4 +70,7 @@ export interface SelfAPI {
|
||||
getLocale: () => Promise<Result<LocaleCode>>;
|
||||
setLocale: (params: SetLocaleParams) => Promise<Result<void>>;
|
||||
|
||||
// ==================== Dialog ====================
|
||||
showSaveDialog: (params: ShowSaveDialogParams) => Promise<Result<string | null>>;
|
||||
saveFileContent: (params: SaveFileContentParams) => Promise<Result<void>>;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,10 @@ const api: SelfAPI = {
|
||||
getLocale: () => ipcRenderer.invoke("getLocale"),
|
||||
setLocale: (params) => ipcRenderer.invoke("setLocale", params),
|
||||
|
||||
// Dialog
|
||||
showSaveDialog: (params) => ipcRenderer.invoke("showSaveDialog", params),
|
||||
saveFileContent: (params) => ipcRenderer.invoke("saveFileContent", params),
|
||||
|
||||
};
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
ConfigProvider,
|
||||
App as AntApp,
|
||||
Space,
|
||||
Modal
|
||||
} from "antd";
|
||||
|
||||
import { Button, Tooltip, Modal } from "@lobehub/ui";
|
||||
import { Button, Tooltip } from "@lobehub/ui";
|
||||
|
||||
import {
|
||||
Cloud,
|
||||
@@ -327,6 +328,7 @@ const App: React.FC = () => {
|
||||
onCancel={() => setSettingsOpen(false)}
|
||||
footer={null}
|
||||
width={480}
|
||||
mask={{ closable: false }}
|
||||
>
|
||||
<Settings />
|
||||
</Modal>
|
||||
|
||||
@@ -5,20 +5,18 @@
|
||||
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Descriptions,
|
||||
Tabs,
|
||||
Spin,
|
||||
Tag,
|
||||
Space,
|
||||
} from "antd";
|
||||
import { Spin, Tag, Space, message } from "antd";
|
||||
import { Button, Empty } from "@lobehub/ui";
|
||||
import {
|
||||
LayoutGrid,
|
||||
Download,
|
||||
History,
|
||||
Code,
|
||||
FileCode,
|
||||
FileText,
|
||||
Monitor,
|
||||
Smartphone,
|
||||
ArrowLeft,
|
||||
} from "lucide-react";
|
||||
import { createStyles } from "antd-style";
|
||||
import { useAppStore } from "@renderer/stores";
|
||||
@@ -26,6 +24,7 @@ import { useDomainStore } from "@renderer/stores";
|
||||
import { CodeViewer } from "../CodeViewer";
|
||||
import { FileConfigResponse, isFileResource } from "@shared/types/kintone";
|
||||
import { getDisplayName, getFileKey } from "@shared/utils/fileDisplay";
|
||||
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
container: css`
|
||||
height: 100%;
|
||||
@@ -34,6 +33,9 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
background: ${token.colorBgContainer};
|
||||
`,
|
||||
header: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: ${token.paddingMD}px ${token.paddingLG}px;
|
||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
||||
`,
|
||||
@@ -41,7 +43,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${token.paddingSM}px;
|
||||
margin-bottom: ${token.marginSM}px;
|
||||
`,
|
||||
appName: css`
|
||||
font-size: ${token.fontSizeHeading5}px;
|
||||
@@ -59,39 +60,84 @@ const useStyles = createStyles(({ token, css }) => ({
|
||||
align-items: center;
|
||||
height: 300px;
|
||||
`,
|
||||
// Table-like file list styles
|
||||
fileTable: css`
|
||||
border: 1px solid ${token.colorBorderSecondary};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
overflow: hidden;
|
||||
`,
|
||||
fileItem: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
||||
border: 1px solid ${token.colorBorderSecondary};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
margin-bottom: ${token.marginSM}px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background 0.2s;
|
||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: ${token.colorPrimary};
|
||||
background: ${token.colorPrimaryBg};
|
||||
background: ${token.colorBgTextHover};
|
||||
}
|
||||
`,
|
||||
fileInfo: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${token.paddingSM}px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`,
|
||||
fileIcon: css`
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
fileName: css`
|
||||
font-weight: 500;
|
||||
`,
|
||||
fileType: css`
|
||||
font-size: ${token.fontSizeSM}px;
|
||||
color: ${token.colorTextSecondary};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`,
|
||||
emptySection: css`
|
||||
padding: ${token.paddingLG}px;
|
||||
text-align: center;
|
||||
color: ${token.colorTextSecondary};
|
||||
`,
|
||||
sectionHeader: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: ${token.paddingXS}px;
|
||||
padding: ${token.paddingSM}px 0;
|
||||
margin-top: ${token.marginMD}px;
|
||||
font-weight: ${token.fontWeightStrong};
|
||||
color: ${token.colorTextSecondary};
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
`,
|
||||
sectionTitle: css`
|
||||
font-size: ${token.fontSizeSM}px;
|
||||
`,
|
||||
// Back button - no border, left aligned with text
|
||||
backButton: css`
|
||||
padding: 0;
|
||||
margin-bottom: ${token.marginMD}px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`,
|
||||
codeView: css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`,
|
||||
}));
|
||||
|
||||
const AppDetail: React.FC = () => {
|
||||
@@ -101,6 +147,26 @@ const AppDetail: React.FC = () => {
|
||||
const { currentApp, selectedAppId, loading, setCurrentApp, setLoading } =
|
||||
useAppStore();
|
||||
|
||||
// View mode: 'list' shows file sections, 'code' shows code viewer
|
||||
const [viewMode, setViewMode] = React.useState<"list" | "code">("list");
|
||||
const [selectedFile, setSelectedFile] = React.useState<{
|
||||
type: "js" | "css";
|
||||
fileKey: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
// Download state: track which file is being downloaded
|
||||
const [downloadingKey, setDownloadingKey] = React.useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [downloadingAll, setDownloadingAll] = React.useState(false);
|
||||
|
||||
// Reset view mode when app changes
|
||||
React.useEffect(() => {
|
||||
setViewMode("list");
|
||||
setSelectedFile(null);
|
||||
}, [selectedAppId]);
|
||||
|
||||
// Load app detail when selected
|
||||
React.useEffect(() => {
|
||||
if (currentDomain && selectedAppId) {
|
||||
@@ -127,13 +193,6 @@ const AppDetail: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState("info");
|
||||
const [selectedFile, setSelectedFile] = React.useState<{
|
||||
type: "js" | "css";
|
||||
fileKey?: string;
|
||||
name: string;
|
||||
} | null>(null);
|
||||
|
||||
if (!currentDomain || !selectedAppId) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
@@ -168,20 +227,139 @@ const AppDetail: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const handleFileClick = (
|
||||
type: "js" | "css",
|
||||
fileKey: string,
|
||||
name: string,
|
||||
) => {
|
||||
setSelectedFile({ type, fileKey, name });
|
||||
setViewMode("code");
|
||||
};
|
||||
|
||||
const handleBackToList = () => {
|
||||
setViewMode("list");
|
||||
setSelectedFile(null);
|
||||
};
|
||||
|
||||
// Download single file with save dialog
|
||||
const handleDownloadFile = async (
|
||||
fileKey: string,
|
||||
fileName: string,
|
||||
type: "js" | "css",
|
||||
) => {
|
||||
if (!currentDomain || downloadingKey) return;
|
||||
|
||||
// Check if fileName already has extension
|
||||
const hasExt = /\.(js|css)$/i.test(fileName);
|
||||
const finalFileName = hasExt ? fileName : `${fileName}.${type}`;
|
||||
|
||||
setDownloadingKey(fileKey);
|
||||
|
||||
try {
|
||||
// 1. Show save dialog
|
||||
const dialogResult = await window.api.showSaveDialog({
|
||||
defaultPath: finalFileName,
|
||||
});
|
||||
|
||||
if (!dialogResult.success || !dialogResult.data) {
|
||||
// User cancelled
|
||||
setDownloadingKey(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const savePath = dialogResult.data;
|
||||
|
||||
// 2. Get file content
|
||||
const contentResult = await window.api.getFileContent({
|
||||
domainId: currentDomain.id,
|
||||
fileKey,
|
||||
});
|
||||
|
||||
if (!contentResult.success || !contentResult.data.content) {
|
||||
message.error(
|
||||
contentResult.success
|
||||
? t("downloadFailed", { ns: "common" })
|
||||
: contentResult.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Save to selected path
|
||||
const saveResult = await window.api.saveFileContent({
|
||||
filePath: savePath,
|
||||
content: contentResult.data.content,
|
||||
});
|
||||
|
||||
if (saveResult.success) {
|
||||
message.success(t("downloadSuccess", { ns: "common" }));
|
||||
} else {
|
||||
message.error(
|
||||
saveResult.error || t("downloadFailed", { ns: "common" }),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Download failed:", error);
|
||||
message.error(t("downloadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setDownloadingKey(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Download all files
|
||||
const handleDownloadAll = async () => {
|
||||
if (!currentDomain || !selectedAppId || downloadingAll) return;
|
||||
|
||||
setDownloadingAll(true);
|
||||
|
||||
try {
|
||||
const result = await window.api.download({
|
||||
domainId: currentDomain.id,
|
||||
appId: selectedAppId,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
message.success(t("downloadAllSuccess", { path: result.path }));
|
||||
} else {
|
||||
message.error(result.error || t("downloadFailed", { ns: "common" }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Download all failed:", error);
|
||||
message.error(t("downloadFailed", { ns: "common" }));
|
||||
} finally {
|
||||
setDownloadingAll(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Icon for file type: FileCode for JS, FileText for CSS
|
||||
const getFileTypeIcon = (type: "js" | "css") => {
|
||||
return type === "js" ? (
|
||||
<FileCode size={16} style={{ color: "#f7df1e" }} />
|
||||
) : (
|
||||
<FileText size={16} style={{ color: "#264de4" }} />
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileList = (
|
||||
files: (FileConfigResponse)[] | undefined,
|
||||
files: FileConfigResponse[] | undefined,
|
||||
type: "js" | "css",
|
||||
) => {
|
||||
if (!files || files.length === 0) {
|
||||
return <div className={styles.emptySection}>{t("noConfig")}</div>;
|
||||
return (
|
||||
<div className={styles.fileTable}>
|
||||
<div className={styles.emptySection} style={{ padding: "16px" }}>
|
||||
{t("noConfig")}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.fileTable}>
|
||||
{files.map((file, index) => {
|
||||
const fileName = getDisplayName(file, type, index);
|
||||
const fileKey = getFileKey(file);
|
||||
const canView = isFileResource(file);
|
||||
const isDownloading = fileKey === downloadingKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -189,20 +367,13 @@ const AppDetail: React.FC = () => {
|
||||
className={styles.fileItem}
|
||||
onClick={() => {
|
||||
if (fileKey) {
|
||||
setSelectedFile({ type, fileKey, name: fileName });
|
||||
setActiveTab("code");
|
||||
handleFileClick(type, fileKey, fileName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={styles.fileInfo}>
|
||||
<FileText size={16} />
|
||||
<div>
|
||||
<div className={styles.fileName}>{fileName}</div>
|
||||
<div className={styles.fileType}>
|
||||
{type.toUpperCase()} ·{" "}
|
||||
{t("fileUpload")}
|
||||
</div>
|
||||
</div>
|
||||
<span className={styles.fileIcon}>{getFileTypeIcon(type)}</span>
|
||||
<span className={styles.fileName}>{fileName}</span>
|
||||
</div>
|
||||
{canView && (
|
||||
<Space>
|
||||
@@ -212,13 +383,25 @@ const AppDetail: React.FC = () => {
|
||||
icon={<Code size={16} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedFile({ type, fileKey, name: fileName });
|
||||
setActiveTab("code");
|
||||
if (fileKey) {
|
||||
handleFileClick(type, fileKey, fileName);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("view")}
|
||||
</Button>
|
||||
<Button type="text" size="small" icon={<Download size={16} />}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<Download size={16} />}
|
||||
loading={isDownloading}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (fileKey) {
|
||||
handleDownloadFile(fileKey, fileName, type);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("download", { ns: "common" })}
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -230,6 +413,23 @@ const AppDetail: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderFileSection = (
|
||||
title: string,
|
||||
icon: React.ReactNode,
|
||||
files: FileConfigResponse[] | undefined,
|
||||
type: "js" | "css",
|
||||
) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.sectionHeader}>
|
||||
{icon}
|
||||
<span className={styles.sectionTitle}>{title}</span>
|
||||
</div>
|
||||
{renderFileList(files, type)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.header}>
|
||||
@@ -239,96 +439,67 @@ const AppDetail: React.FC = () => {
|
||||
<Tag color="blue">{currentApp.appId}</Tag>
|
||||
</div>
|
||||
<Space>
|
||||
<Button icon={<History size={16} />}>{t("versionHistory", { ns: "common" })}</Button>
|
||||
<Button type="primary" icon={<Download size={16} />}>
|
||||
<Button icon={<History size={16} />}>
|
||||
{t("versionHistory", { ns: "common" })}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<Download size={16} />}
|
||||
loading={downloadingAll}
|
||||
onClick={handleDownloadAll}
|
||||
>
|
||||
{t("downloadAll")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<div className={styles.content}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={[
|
||||
{
|
||||
key: "info",
|
||||
label: t("basicInfo"),
|
||||
children: (
|
||||
<Descriptions column={2} bordered size="small">
|
||||
<Descriptions.Item label={t("appId")}>
|
||||
{currentApp.appId}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("appCode")}>
|
||||
{currentApp.code || "-"}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("createdAt")}>
|
||||
{currentApp.createdAt}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("creator")}>
|
||||
{currentApp.creator?.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("modifiedAt")}>
|
||||
{currentApp.modifiedAt}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("modifier")}>
|
||||
{currentApp.modifier?.name}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("spaceId")} span={2}>
|
||||
{currentApp.spaceId || "-"}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "pc-js",
|
||||
label: t("pcJs"),
|
||||
children: renderFileList(
|
||||
{viewMode === "list" ? (
|
||||
<>
|
||||
{renderFileSection(
|
||||
t("pcJs"),
|
||||
<Monitor size={14} />,
|
||||
currentApp.customization?.desktop?.js,
|
||||
"js",
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "pc-css",
|
||||
label: t("pcCss"),
|
||||
children: renderFileList(
|
||||
)}
|
||||
{renderFileSection(
|
||||
t("pcCss"),
|
||||
<Monitor size={14} />,
|
||||
currentApp.customization?.desktop?.css,
|
||||
"css",
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "mobile-js",
|
||||
label: t("mobileJs"),
|
||||
children: renderFileList(
|
||||
)}
|
||||
{renderFileSection(
|
||||
t("mobileJs"),
|
||||
<Smartphone size={14} />,
|
||||
currentApp.customization?.mobile?.js,
|
||||
"js",
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "mobile-css",
|
||||
label: t("mobileCss"),
|
||||
children: renderFileList(
|
||||
)}
|
||||
{renderFileSection(
|
||||
t("mobileCss"),
|
||||
<Smartphone size={14} />,
|
||||
currentApp.customization?.mobile?.css,
|
||||
"css",
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "code",
|
||||
label: t("codeView"),
|
||||
children: selectedFile && selectedFile.fileKey ? (
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.codeView}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeft size={16} />}
|
||||
onClick={handleBackToList}
|
||||
className={styles.backButton}
|
||||
>
|
||||
{t("backToList")}
|
||||
</Button>
|
||||
{selectedFile && (
|
||||
<CodeViewer
|
||||
fileKey={selectedFile.fileKey}
|
||||
fileName={selectedFile.name}
|
||||
fileType={selectedFile.type}
|
||||
/>
|
||||
) : (
|
||||
<div className={styles.emptySection}>
|
||||
{t("selectFileToView")}
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Form, Modal } from "@lobehub/ui";
|
||||
import { Input, message } from "antd";
|
||||
import { Button, Form } from "@lobehub/ui";
|
||||
import { Input, message, Modal } from "antd";
|
||||
import { createStyles } from "antd-style";
|
||||
import { useDomainStore } from "@renderer/stores";
|
||||
import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc";
|
||||
@@ -14,7 +14,7 @@ import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc";
|
||||
const useStyles = createStyles(({ token, css }) => ({
|
||||
form: css`
|
||||
.ant-form-item {
|
||||
margin-bottom: ${token.marginMD}px;
|
||||
margin-bottom: ${token.marginSM}px;
|
||||
}
|
||||
`,
|
||||
passwordHint: css`
|
||||
|
||||
@@ -34,5 +34,6 @@
|
||||
"unpin": "Unpin",
|
||||
"pinApp": "Pin App",
|
||||
"selectDomainFirst": "Please select a Domain first",
|
||||
"loadAppsFailed": "Failed to load apps"
|
||||
"loadAppsFailed": "Failed to load apps",
|
||||
"backToList": "Back to List"
|
||||
}
|
||||
|
||||
@@ -32,5 +32,8 @@
|
||||
"selectAll": "Select all",
|
||||
"deployFiles": "Deploy Files",
|
||||
"versionHistory": "Version History",
|
||||
"settings": "Settings"
|
||||
"settings": "Settings",
|
||||
"downloadSuccess": "Download successful",
|
||||
"downloadFailed": "Download failed",
|
||||
"downloadAllSuccess": "Downloaded to: {{path}}"
|
||||
}
|
||||
|
||||
@@ -34,5 +34,6 @@
|
||||
"unpin": "ピン留め解除",
|
||||
"pinApp": "アプリをピン留め",
|
||||
"selectDomainFirst": "最初にドメインを選択してください",
|
||||
"loadAppsFailed": "アプリの読み込みに失敗しました"
|
||||
"loadAppsFailed": "アプリの読み込みに失敗しました",
|
||||
"backToList": "リストに戻る"
|
||||
}
|
||||
|
||||
@@ -32,5 +32,8 @@
|
||||
"selectAll": "すべて選択",
|
||||
"deployFiles": "ファイルをデプロイ",
|
||||
"versionHistory": "バージョン履歴",
|
||||
"settings": "設定"
|
||||
"settings": "設定",
|
||||
"downloadSuccess": "ダウンロード成功",
|
||||
"downloadFailed": "ダウンロード失敗",
|
||||
"downloadAllSuccess": "ダウンロード先: {{path}}"
|
||||
}
|
||||
|
||||
@@ -34,5 +34,6 @@
|
||||
"unpin": "取消置顶",
|
||||
"pinApp": "置顶应用",
|
||||
"selectDomainFirst": "请先选择一个 Domain",
|
||||
"loadAppsFailed": "加载应用失败"
|
||||
"loadAppsFailed": "加载应用失败",
|
||||
"backToList": "返回列表"
|
||||
}
|
||||
|
||||
@@ -32,5 +32,8 @@
|
||||
"selectAll": "全选",
|
||||
"deployFiles": "部署文件",
|
||||
"versionHistory": "版本历史",
|
||||
"settings": "设置"
|
||||
"settings": "设置",
|
||||
"downloadSuccess": "下载成功",
|
||||
"downloadFailed": "下载失败",
|
||||
"downloadAllSuccess": "已下载到: {{path}}"
|
||||
}
|
||||
|
||||
@@ -116,6 +116,16 @@ export interface SetLocaleParams {
|
||||
locale: import("./locale").LocaleCode;
|
||||
}
|
||||
|
||||
// ==================== Dialog IPC Types ====================
|
||||
|
||||
export interface ShowSaveDialogParams {
|
||||
defaultPath?: string;
|
||||
}
|
||||
|
||||
export interface SaveFileContentParams {
|
||||
filePath: string;
|
||||
content: string; // Base64 encoded
|
||||
}
|
||||
// ==================== IPC API Interface ====================
|
||||
|
||||
export interface ElectronAPI {
|
||||
@@ -146,4 +156,8 @@ export interface ElectronAPI {
|
||||
getVersions: (params: GetVersionsParams) => Promise<Result<Version[]>>;
|
||||
deleteVersion: (id: string) => Promise<Result<void>>;
|
||||
rollback: (params: RollbackParams) => Promise<DeployResult>;
|
||||
|
||||
// Dialog
|
||||
showSaveDialog: (params: ShowSaveDialogParams) => Promise<Result<string | null>>;
|
||||
saveFileContent: (params: SaveFileContentParams) => Promise<Result<void>>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user