fix UI
This commit is contained in:
@@ -295,12 +295,14 @@ interface Domain {
|
|||||||
- 显示 App 名称、App ID、所属 Space ID(若有)、创建时间
|
- 显示 App 名称、App ID、所属 Space ID(若有)、创建时间
|
||||||
- App 数据持久化存储,下次打开应用时自动加载
|
- App 数据持久化存储,下次打开应用时自动加载
|
||||||
- 支持重新加载(覆盖已有数据)
|
- 支持重新加载(覆盖已有数据)
|
||||||
|
HW|
|
||||||
**FR-BROWSE-002**: 分页显示
|
BB|**FR-BROWSE-002**: 列表显示
|
||||||
- App 列表支持分页,默认每页 20 条
|
- App 列表使用可点击列表(List 组件)展示,无需分页
|
||||||
- 显示总数量和当前页码
|
- 全量显示所有 App,显示 App 名称和 App ID
|
||||||
- 支持切换页码
|
- 点击列表项即可选择 App
|
||||||
|
- 显示总数量和加载时间
|
||||||
|
- 所属 Space 暂时不显示(后续版本支持)
|
||||||
|
NK|
|
||||||
**FR-BROWSE-003**: 搜索过滤
|
**FR-BROWSE-003**: 搜索过滤
|
||||||
- 支持按 App 名称搜索
|
- 支持按 App 名称搜索
|
||||||
- 搜索结果实时过滤
|
- 搜索结果实时过滤
|
||||||
|
|||||||
@@ -106,11 +106,26 @@ function registerGetDomains(): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new domain
|
* Create a new domain
|
||||||
|
* Deduplication: Check if domain+username already exists
|
||||||
*/
|
*/
|
||||||
function registerCreateDomain(): void {
|
function registerCreateDomain(): void {
|
||||||
handleWithParams<CreateDomainParams, Domain>(
|
handleWithParams<CreateDomainParams, Domain>(
|
||||||
"createDomain",
|
"createDomain",
|
||||||
async (params) => {
|
async (params) => {
|
||||||
|
// Check for duplicate domain+username
|
||||||
|
const existingDomains = await listDomains();
|
||||||
|
const duplicate = existingDomains.find(
|
||||||
|
(d) =>
|
||||||
|
d.domain.toLowerCase() === params.domain.toLowerCase() &&
|
||||||
|
d.username.toLowerCase() === params.username.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicate) {
|
||||||
|
throw new Error(
|
||||||
|
`Domain "${params.domain}" with user "${params.username}" already exists. Please edit the existing domain instead.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const domain: Domain = {
|
const domain: Domain = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ import {
|
|||||||
theme,
|
theme,
|
||||||
ConfigProvider,
|
ConfigProvider,
|
||||||
App as AntApp,
|
App as AntApp,
|
||||||
Tabs,
|
|
||||||
Button,
|
Button,
|
||||||
Space,
|
Space,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
Tooltip,
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
GithubOutlined,
|
GithubOutlined,
|
||||||
CloudServerOutlined,
|
CloudServerOutlined,
|
||||||
AppstoreOutlined,
|
|
||||||
CloudUploadOutlined,
|
CloudUploadOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
|
LeftOutlined,
|
||||||
|
RightOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import zhCN from "antd/locale/zh_CN";
|
import zhCN from "antd/locale/zh_CN";
|
||||||
@@ -32,11 +33,14 @@ import { AppDetail } from "@renderer/components/AppDetail";
|
|||||||
import { DeployDialog } from "@renderer/components/DeployDialog";
|
import { DeployDialog } from "@renderer/components/DeployDialog";
|
||||||
|
|
||||||
const { Header, Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
const { Title, Text } = Typography;
|
const { Title } = Typography;
|
||||||
|
|
||||||
// Domain section heights
|
// Domain section heights
|
||||||
const DOMAIN_SECTION_COLLAPSED = 56; // Just show current domain
|
const DOMAIN_SECTION_COLLAPSED = 56;
|
||||||
const DOMAIN_SECTION_EXPANDED = 240; // Show full list
|
const DOMAIN_SECTION_EXPANDED = 240;
|
||||||
|
const DEFAULT_SIDER_WIDTH = 320;
|
||||||
|
const MIN_SIDER_WIDTH = 280;
|
||||||
|
const MAX_SIDER_WIDTH = 500;
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
layout: css`
|
layout: css`
|
||||||
@@ -52,12 +56,17 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
background: ${token.colorBgContainer};
|
background: ${token.colorBgContainer};
|
||||||
border-right: 1px solid ${token.colorBorderSecondary};
|
border-right: 1px solid ${token.colorBorderSecondary};
|
||||||
`,
|
`,
|
||||||
|
siderCollapsed: css`
|
||||||
|
width: 0 !important;
|
||||||
|
min-width: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
`,
|
||||||
logo: css`
|
logo: css`
|
||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 8px 16px;
|
margin: 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
border-bottom: 1px solid ${token.colorBorderSecondary};
|
||||||
`,
|
`,
|
||||||
@@ -72,7 +81,11 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`,
|
`,
|
||||||
mainLayout: css`
|
mainLayout: css`
|
||||||
margin-left: 280px;
|
margin-left: ${DEFAULT_SIDER_WIDTH}px;
|
||||||
|
transition: margin-left 0.2s;
|
||||||
|
`,
|
||||||
|
mainLayoutCollapsed: css`
|
||||||
|
margin-left: 0;
|
||||||
transition: margin-left 0.2s;
|
transition: margin-left 0.2s;
|
||||||
`,
|
`,
|
||||||
header: css`
|
header: css`
|
||||||
@@ -94,12 +107,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`,
|
`,
|
||||||
leftPanel: css`
|
|
||||||
width: 300px;
|
|
||||||
border-right: 1px solid ${token.colorBorderSecondary};
|
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
`,
|
|
||||||
rightPanel: css`
|
rightPanel: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -114,6 +121,28 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: height 0.2s ease-in-out;
|
transition: height 0.2s ease-in-out;
|
||||||
`,
|
`,
|
||||||
|
resizeHandle: css`
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 4px;
|
||||||
|
cursor: col-resize;
|
||||||
|
background: transparent;
|
||||||
|
transition: background 0.2s;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${token.colorPrimary};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
collapseButton: css`
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: left 0.2s;
|
||||||
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
@@ -125,42 +154,122 @@ const App: React.FC = () => {
|
|||||||
const { currentDomain } = useDomainStore();
|
const { currentDomain } = useDomainStore();
|
||||||
const [deployDialogOpen, setDeployDialogOpen] = React.useState(false);
|
const [deployDialogOpen, setDeployDialogOpen] = React.useState(false);
|
||||||
const [domainExpanded, setDomainExpanded] = React.useState(true);
|
const [domainExpanded, setDomainExpanded] = React.useState(true);
|
||||||
|
const [siderCollapsed, setSiderCollapsed] = React.useState(false);
|
||||||
|
const [siderWidth, setSiderWidth] = React.useState(DEFAULT_SIDER_WIDTH);
|
||||||
|
const [isResizing, setIsResizing] = React.useState(false);
|
||||||
|
|
||||||
const domainSectionHeight = domainExpanded
|
const domainSectionHeight = domainExpanded
|
||||||
? DOMAIN_SECTION_EXPANDED
|
? DOMAIN_SECTION_EXPANDED
|
||||||
: DOMAIN_SECTION_COLLAPSED;
|
: DOMAIN_SECTION_COLLAPSED;
|
||||||
|
|
||||||
|
// Handle resize start
|
||||||
|
const handleResizeStart = React.useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsResizing(true);
|
||||||
|
|
||||||
|
const startX = e.clientX;
|
||||||
|
const startWidth = siderWidth;
|
||||||
|
|
||||||
|
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||||
|
const delta = moveEvent.clientX - startX;
|
||||||
|
const newWidth = Math.min(
|
||||||
|
MAX_SIDER_WIDTH,
|
||||||
|
Math.max(MIN_SIDER_WIDTH, startWidth + delta),
|
||||||
|
);
|
||||||
|
setSiderWidth(newWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
setIsResizing(false);
|
||||||
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
|
},
|
||||||
|
[siderWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleSider = () => {
|
||||||
|
setSiderCollapsed(!siderCollapsed);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider locale={zhCN}>
|
||||||
<AntApp>
|
<AntApp>
|
||||||
<Layout className={styles.layout}>
|
<Layout className={styles.layout}>
|
||||||
{/* Left Sider - Domain List & App List */}
|
{/* Left Sider - Domain List & App List */}
|
||||||
<Sider width={280} className={styles.sider}>
|
<Sider
|
||||||
<div className={styles.logo}>
|
width={siderWidth}
|
||||||
<CloudServerOutlined style={{ fontSize: 24, color: "#1890ff" }} />
|
className={`${styles.sider} ${siderCollapsed ? styles.siderCollapsed : ""}`}
|
||||||
<span className={styles.logoText}>Kintone Manager</span>
|
style={{ width: siderCollapsed ? 0 : siderWidth }}
|
||||||
</div>
|
>
|
||||||
<div className={styles.siderContent}>
|
{!siderCollapsed && (
|
||||||
<div
|
<>
|
||||||
className={styles.domainSection}
|
<div className={styles.logo}>
|
||||||
style={{ height: domainSectionHeight }}
|
<div
|
||||||
>
|
style={{ display: "flex", alignItems: "center", gap: 8 }}
|
||||||
<DomainManager
|
>
|
||||||
collapsed={!domainExpanded}
|
<CloudServerOutlined
|
||||||
onToggleCollapse={() => setDomainExpanded(!domainExpanded)}
|
style={{ fontSize: 24, color: "#1890ff" }}
|
||||||
|
/>
|
||||||
|
<span className={styles.logoText}>Kintone Manager</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.siderContent}>
|
||||||
|
<div
|
||||||
|
className={styles.domainSection}
|
||||||
|
style={{ height: domainSectionHeight }}
|
||||||
|
>
|
||||||
|
<DomainManager
|
||||||
|
collapsed={!domainExpanded}
|
||||||
|
onToggleCollapse={() =>
|
||||||
|
setDomainExpanded(!domainExpanded)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={styles.appSection}
|
||||||
|
style={{ height: `calc(100% - ${domainSectionHeight}px)` }}
|
||||||
|
>
|
||||||
|
<AppList />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Resize handle */}
|
||||||
|
<div
|
||||||
|
className={styles.resizeHandle}
|
||||||
|
onMouseDown={handleResizeStart}
|
||||||
|
style={{ background: isResizing ? "#1890ff" : undefined }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</>
|
||||||
<div
|
)}
|
||||||
className={styles.appSection}
|
|
||||||
style={{ height: `calc(100% - ${domainSectionHeight}px)` }}
|
|
||||||
>
|
|
||||||
<AppList />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Sider>
|
</Sider>
|
||||||
|
|
||||||
|
{/* Collapse/Expand Button */}
|
||||||
|
<div
|
||||||
|
className={styles.collapseButton}
|
||||||
|
style={{ left: siderCollapsed ? 0 : siderWidth }}
|
||||||
|
>
|
||||||
|
<Tooltip title={siderCollapsed ? "展开侧边栏" : "收起侧边栏"}>
|
||||||
|
<Button
|
||||||
|
type="default"
|
||||||
|
icon={siderCollapsed ? <RightOutlined /> : <LeftOutlined />}
|
||||||
|
onClick={toggleSider}
|
||||||
|
style={{
|
||||||
|
borderRadius: "0 4px 4px 0",
|
||||||
|
boxShadow: "2px 0 8px rgba(0,0,0,0.15)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<Layout className={styles.mainLayout}>
|
<Layout
|
||||||
|
className={`${styles.mainLayout} ${siderCollapsed ? styles.mainLayoutCollapsed : ""}`}
|
||||||
|
style={{ marginLeft: siderCollapsed ? 0 : siderWidth }}
|
||||||
|
>
|
||||||
<Header className={styles.header}>
|
<Header className={styles.header}>
|
||||||
<Title level={5} style={{ margin: 0 }}>
|
<Title level={5} style={{ margin: 0 }}>
|
||||||
{currentDomain
|
{currentDomain
|
||||||
|
|||||||
@@ -1,29 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* AppList Component
|
* AppList Component
|
||||||
* Displays apps in a table with pagination, search, and load functionality
|
* Displays apps in a clickable list (no pagination)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { List, Button, Input, Empty, Spin, Typography, Space } from "antd";
|
||||||
Table,
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Empty,
|
|
||||||
Spin,
|
|
||||||
Typography,
|
|
||||||
Tag,
|
|
||||||
Space,
|
|
||||||
Tooltip,
|
|
||||||
Pagination,
|
|
||||||
} from "antd";
|
|
||||||
import {
|
import {
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
ClockCircleOutlined,
|
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import type { ColumnsType } from "antd/es/table";
|
|
||||||
import { useAppStore } from "@renderer/stores";
|
import { useAppStore } from "@renderer/stores";
|
||||||
import { useDomainStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import type { KintoneApp } from "@shared/types/kintone";
|
import type { KintoneApp } from "@shared/types/kintone";
|
||||||
@@ -44,6 +31,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: ${token.paddingSM}px;
|
gap: ${token.paddingSM}px;
|
||||||
|
flex-shrink: 0;
|
||||||
`,
|
`,
|
||||||
searchWrapper: css`
|
searchWrapper: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -52,14 +40,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
content: css`
|
content: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: ${token.paddingSM}px;
|
|
||||||
`,
|
|
||||||
footer: css`
|
|
||||||
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
|
||||||
border-top: 1px solid ${token.colorBorderSecondary};
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
`,
|
`,
|
||||||
loading: css`
|
loading: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -74,17 +54,52 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
`,
|
`,
|
||||||
spaceId: css`
|
listItem: css`
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
padding: ${token.paddingSM}px ${token.paddingMD}px !important;
|
||||||
|
border-bottom: 1px solid ${token.colorBorderSecondary} !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${token.colorBgTextHover};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
listItemActive: css`
|
||||||
|
background: ${token.colorPrimaryBgHover} !important;
|
||||||
|
border-left: 3px solid ${token.colorPrimary} !important;
|
||||||
|
`,
|
||||||
|
appInfo: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${token.paddingSM}px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
`,
|
||||||
|
appName: css`
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
`,
|
||||||
|
appId: css`
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
flex-shrink: 0;
|
||||||
|
`,
|
||||||
|
footer: css`
|
||||||
|
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
||||||
|
border-top: 1px solid ${token.colorBorderSecondary};
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
`,
|
`,
|
||||||
loadedInfo: css`
|
loadedInfo: css`
|
||||||
color: ${token.colorTextSecondary};
|
color: ${token.colorTextSecondary};
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
`,
|
`,
|
||||||
clockIcon: css`
|
|
||||||
color: ${token.colorTextSecondary};
|
|
||||||
`,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const AppList: React.FC = () => {
|
const AppList: React.FC = () => {
|
||||||
@@ -94,18 +109,14 @@ const AppList: React.FC = () => {
|
|||||||
apps,
|
apps,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
currentPage,
|
|
||||||
pageSize,
|
|
||||||
searchText,
|
searchText,
|
||||||
loadedAt,
|
loadedAt,
|
||||||
|
selectedAppId,
|
||||||
setApps,
|
setApps,
|
||||||
setLoading,
|
setLoading,
|
||||||
setError,
|
setError,
|
||||||
setCurrentPage,
|
|
||||||
setPageSize,
|
|
||||||
setSearchText,
|
setSearchText,
|
||||||
setSelectedAppId,
|
setSelectedAppId,
|
||||||
setCurrentApp,
|
|
||||||
} = useAppStore();
|
} = useAppStore();
|
||||||
|
|
||||||
// Load apps from Kintone
|
// Load apps from Kintone
|
||||||
@@ -126,7 +137,6 @@ const AppList: React.FC = () => {
|
|||||||
(a, b) => parseInt(b.appId, 10) - parseInt(a.appId, 10),
|
(a, b) => parseInt(b.appId, 10) - parseInt(a.appId, 10),
|
||||||
);
|
);
|
||||||
setApps(sortedApps);
|
setApps(sortedApps);
|
||||||
setCurrentPage(1);
|
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || "加载应用失败");
|
setError(result.error || "加载应用失败");
|
||||||
}
|
}
|
||||||
@@ -145,80 +155,34 @@ const AppList: React.FC = () => {
|
|||||||
(app) =>
|
(app) =>
|
||||||
app.name.toLowerCase().includes(lowerSearch) ||
|
app.name.toLowerCase().includes(lowerSearch) ||
|
||||||
app.appId.includes(searchText) ||
|
app.appId.includes(searchText) ||
|
||||||
(app.code && app.code.toLowerCase().includes(lowerSearch)) ||
|
(app.code && app.code.toLowerCase().includes(lowerSearch)),
|
||||||
(app.spaceId && app.spaceId.includes(searchText)),
|
|
||||||
);
|
);
|
||||||
}, [apps, searchText]);
|
}, [apps, searchText]);
|
||||||
|
|
||||||
// Paginated apps
|
// Handle item click
|
||||||
const paginatedApps = React.useMemo(() => {
|
const handleItemClick = (app: KintoneApp) => {
|
||||||
const start = (currentPage - 1) * pageSize;
|
setSelectedAppId(app.appId);
|
||||||
const end = start + pageSize;
|
|
||||||
return filteredApps.slice(start, end);
|
|
||||||
}, [filteredApps, currentPage, pageSize]);
|
|
||||||
|
|
||||||
// Handle row click
|
|
||||||
const handleRowClick = (record: KintoneApp) => {
|
|
||||||
setSelectedAppId(record.appId);
|
|
||||||
// The AppDetail component will fetch the full app details
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Table columns
|
// Render list item
|
||||||
const columns: ColumnsType<KintoneApp> = [
|
const renderItem = (app: KintoneApp) => {
|
||||||
{
|
const isActive = selectedAppId === app.appId;
|
||||||
title: "App ID",
|
|
||||||
dataIndex: "appId",
|
return (
|
||||||
key: "appId",
|
<List.Item
|
||||||
width: 100,
|
className={`${styles.listItem} ${isActive ? styles.listItemActive : ""}`}
|
||||||
render: (appId: string) => (
|
onClick={() => handleItemClick(app)}
|
||||||
<Text code style={{ fontSize: 12 }}>
|
>
|
||||||
{appId}
|
<div className={styles.appInfo}>
|
||||||
</Text>
|
<AppstoreOutlined style={{ color: "#1890ff", fontSize: 16 }} />
|
||||||
),
|
<span className={styles.appName}>{app.name}</span>
|
||||||
},
|
<Text code className={styles.appId}>
|
||||||
{
|
ID: {app.appId}
|
||||||
title: "应用名称",
|
|
||||||
dataIndex: "name",
|
|
||||||
key: "name",
|
|
||||||
ellipsis: true,
|
|
||||||
render: (name: string) => (
|
|
||||||
<Space>
|
|
||||||
<AppstoreOutlined style={{ color: "#1890ff" }} />
|
|
||||||
<Text strong>{name}</Text>
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "所属 Space",
|
|
||||||
dataIndex: "spaceId",
|
|
||||||
key: "spaceId",
|
|
||||||
width: 120,
|
|
||||||
render: (spaceId?: string) =>
|
|
||||||
spaceId ? (
|
|
||||||
<Tooltip title="Space ID">
|
|
||||||
<Tag color="blue" className={styles.spaceId}>
|
|
||||||
{spaceId}
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Text type="secondary">-</Text>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "创建时间",
|
|
||||||
dataIndex: "createdAt",
|
|
||||||
key: "createdAt",
|
|
||||||
width: 180,
|
|
||||||
render: (createdAt: string) => (
|
|
||||||
<Space size={4}>
|
|
||||||
<ClockCircleOutlined className={styles.clockIcon} />
|
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
|
||||||
{new Date(createdAt).toLocaleString("zh-CN")}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</div>
|
||||||
),
|
</List.Item>
|
||||||
},
|
);
|
||||||
];
|
};
|
||||||
|
|
||||||
if (!currentDomain) {
|
if (!currentDomain) {
|
||||||
return (
|
return (
|
||||||
@@ -270,21 +234,15 @@ const AppList: React.FC = () => {
|
|||||||
</Empty>
|
</Empty>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Table
|
<List
|
||||||
columns={columns}
|
dataSource={filteredApps}
|
||||||
dataSource={paginatedApps}
|
renderItem={renderItem}
|
||||||
rowKey="appId"
|
locale={{ emptyText: "没有匹配的应用" }}
|
||||||
pagination={false}
|
|
||||||
size="small"
|
|
||||||
onRow={(record) => ({
|
|
||||||
onClick: () => handleRowClick(record),
|
|
||||||
style: { cursor: "pointer" },
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer with pagination */}
|
{/* Footer with info */}
|
||||||
{apps.length > 0 && (
|
{apps.length > 0 && (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<div className={styles.loadedInfo}>
|
<div className={styles.loadedInfo}>
|
||||||
@@ -297,24 +255,6 @@ const AppList: React.FC = () => {
|
|||||||
共 {filteredApps.length} 个应用
|
共 {filteredApps.length} 个应用
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Pagination
|
|
||||||
current={currentPage}
|
|
||||||
pageSize={pageSize}
|
|
||||||
total={filteredApps.length}
|
|
||||||
onChange={(page, size) => {
|
|
||||||
setCurrentPage(page);
|
|
||||||
if (size !== pageSize) {
|
|
||||||
setPageSize(size);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showSizeChanger
|
|
||||||
showQuickJumper
|
|
||||||
size="small"
|
|
||||||
pageSizeOptions={["20", "50", "100"]}
|
|
||||||
showTotal={(total) =>
|
|
||||||
`第 ${(currentPage - 1) * pageSize + 1}-${Math.min(currentPage * pageSize, total)} 条`
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,8 +91,16 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// Decode base64 content
|
// Decode base64 content properly for UTF-8 (including Japanese characters)
|
||||||
const decoded = atob(result.data.content || "");
|
const base64 = result.data.content || "";
|
||||||
|
const binaryString = atob(base64);
|
||||||
|
// Decode as UTF-8 to properly handle Japanese and other multi-byte characters
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
const decoded = decoder.decode(bytes);
|
||||||
setContent(decoded);
|
setContent(decoded);
|
||||||
|
|
||||||
// Detect language from file name
|
// Detect language from file name
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
* DomainManager Component
|
* DomainManager Component
|
||||||
* Main container for domain management
|
* Main container for domain management
|
||||||
* Supports collapsed/expanded view
|
* Supports collapsed/expanded view
|
||||||
|
* Expand/collapse triggered by clicking bottom area
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@@ -23,7 +24,6 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: ${token.paddingLG}px;
|
|
||||||
background: ${token.colorBgContainer};
|
background: ${token.colorBgContainer};
|
||||||
`,
|
`,
|
||||||
collapsedContainer: css`
|
collapsedContainer: css`
|
||||||
@@ -37,7 +37,8 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: ${token.marginLG}px;
|
margin-bottom: ${token.marginLG}px;
|
||||||
padding-bottom: ${token.paddingMD}px;
|
padding: 0 ${token.paddingLG}px;
|
||||||
|
padding-top: ${token.paddingLG}px;
|
||||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
border-bottom: 1px solid ${token.colorBorderSecondary};
|
||||||
`,
|
`,
|
||||||
title: css`
|
title: css`
|
||||||
@@ -53,6 +54,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
content: css`
|
content: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
padding: 0 ${token.paddingLG}px;
|
||||||
`,
|
`,
|
||||||
loading: css`
|
loading: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -102,6 +104,24 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
font-size: ${token.fontSizeSM}px;
|
font-size: ${token.fontSizeSM}px;
|
||||||
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
padding: ${token.paddingSM}px ${token.paddingMD}px;
|
||||||
`,
|
`,
|
||||||
|
// Bottom toggle area - click to expand/collapse
|
||||||
|
bottomToggleArea: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: ${token.paddingSM}px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-top: 1px solid ${token.colorBorderSecondary};
|
||||||
|
transition: background 0.2s;
|
||||||
|
color: ${token.colorTextSecondary};
|
||||||
|
font-size: 12px;
|
||||||
|
gap: ${token.paddingXS}px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${token.colorBgTextHover};
|
||||||
|
color: ${token.colorPrimary};
|
||||||
|
}
|
||||||
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface DomainManagerProps {
|
interface DomainManagerProps {
|
||||||
@@ -140,12 +160,11 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
loadDomains();
|
loadDomains();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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.collapsedContainer}>
|
||||||
<div className={styles.collapsedHeader} onClick={onToggleCollapse}>
|
<div className={styles.collapsedHeader}>
|
||||||
<div className={styles.collapsedInfo}>
|
<div className={styles.collapsedInfo}>
|
||||||
<Avatar
|
<Avatar
|
||||||
icon={<CloudServerOutlined />}
|
icon={<CloudServerOutlined />}
|
||||||
@@ -169,26 +188,23 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Tooltip title="添加 Domain">
|
||||||
<Tooltip title="添加 Domain">
|
<Button
|
||||||
<Button
|
type="text"
|
||||||
type="text"
|
size="small"
|
||||||
size="small"
|
icon={<PlusOutlined />}
|
||||||
icon={<PlusOutlined />}
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
handleAdd();
|
||||||
handleAdd();
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</Tooltip>
|
||||||
</Tooltip>
|
</div>
|
||||||
<Tooltip title="展开">
|
|
||||||
<Button
|
{/* Bottom toggle area - click to expand */}
|
||||||
type="text"
|
<div className={styles.bottomToggleArea} onClick={onToggleCollapse}>
|
||||||
size="small"
|
<DownOutlined />
|
||||||
icon={<DownOutlined />}
|
<span>点击展开</span>
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DomainForm
|
<DomainForm
|
||||||
@@ -206,9 +222,6 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<h2 className={styles.title}>Domain 管理</h2>
|
<h2 className={styles.title}>Domain 管理</h2>
|
||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<Tooltip title="收起">
|
|
||||||
<Button icon={<UpOutlined />} onClick={onToggleCollapse} />
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="刷新">
|
<Tooltip title="刷新">
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
@@ -241,6 +254,12 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom toggle area - click to collapse */}
|
||||||
|
<div className={styles.bottomToggleArea} onClick={onToggleCollapse}>
|
||||||
|
<UpOutlined />
|
||||||
|
<span>点击收起</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<DomainForm
|
<DomainForm
|
||||||
open={formOpen}
|
open={formOpen}
|
||||||
onClose={handleCloseForm}
|
onClose={handleCloseForm}
|
||||||
|
|||||||
Reference in New Issue
Block a user