fix UI
This commit is contained in:
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@@ -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"
|
||||||
|
],
|
||||||
|
}
|
||||||
42
MEMORY.md
Normal file
42
MEMORY.md
Normal file
@@ -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`
|
||||||
@@ -31,6 +31,13 @@ function createWindow(): void {
|
|||||||
mainWindow.show();
|
mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 阻止 Alt 键显示默认菜单栏
|
||||||
|
mainWindow.webContents.on("before-input-event", (event, input) => {
|
||||||
|
if (input.key === "Alt" && input.type === "keyDown") {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url);
|
shell.openExternal(details.url);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
|
|||||||
@@ -105,9 +105,13 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`,
|
`,
|
||||||
emptySection: css`
|
emptySection: css`
|
||||||
|
height: 100%;
|
||||||
padding: ${token.paddingLG}px;
|
padding: ${token.paddingLG}px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: ${token.colorTextSecondary};
|
color: ${token.colorTextSecondary};
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
`,
|
`,
|
||||||
sectionHeader: css`
|
sectionHeader: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -199,7 +203,6 @@ const AppDetail: React.FC = () => {
|
|||||||
<div className={styles.emptySection}>
|
<div className={styles.emptySection}>
|
||||||
<Empty
|
<Empty
|
||||||
description={t("selectApp")}
|
description={t("selectApp")}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,7 +223,6 @@ const AppDetail: React.FC = () => {
|
|||||||
<div className={styles.emptySection}>
|
<div className={styles.emptySection}>
|
||||||
<Empty
|
<Empty
|
||||||
description={t("appNotFound")}
|
description={t("appNotFound")}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -429,7 +429,6 @@ const AppList: React.FC = () => {
|
|||||||
<div className={styles.empty}>
|
<div className={styles.empty}>
|
||||||
<Empty
|
<Empty
|
||||||
description={t("noApps")}
|
description={t("noApps")}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
>
|
>
|
||||||
<Button type="primary" onClick={handleLoadApps}>
|
<Button type="primary" onClick={handleLoadApps}>
|
||||||
{t("loadApps")}
|
{t("loadApps")}
|
||||||
|
|||||||
@@ -172,7 +172,6 @@ const CodeViewer: React.FC<CodeViewerProps> = ({
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Empty
|
<Empty
|
||||||
description={t("fileEmpty")}
|
description={t("fileEmpty")}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,24 +5,11 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Button, Form } from "@lobehub/ui";
|
import { Button, Input, InputPassword, Modal } from "@lobehub/ui";
|
||||||
import { Input, message, Modal } from "antd";
|
import { Form } from "antd";
|
||||||
import { createStyles } from "antd-style";
|
|
||||||
import { useDomainStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc";
|
import type { CreateDomainParams, UpdateDomainParams } from "@shared/types/ipc";
|
||||||
|
import { CheckCircle2, XCircle } from "lucide-react";
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
|
||||||
form: css`
|
|
||||||
.ant-form-item {
|
|
||||||
margin-bottom: ${token.marginSM}px;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
passwordHint: css`
|
|
||||||
color: ${token.colorTextSecondary};
|
|
||||||
font-size: ${token.fontSizeSM}px;
|
|
||||||
margin-top: ${token.marginXXS}px;
|
|
||||||
`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface DomainFormProps {
|
interface DomainFormProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -30,20 +17,55 @@ interface DomainFormProps {
|
|||||||
domainId: string | null;
|
domainId: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection test result state
|
||||||
|
type TestResult = {
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
// Test connection parameters type
|
||||||
|
export type TestConnectionParams = {
|
||||||
|
domain: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
processedDomain: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create error type
|
||||||
|
type CreateErrorType = "connection" | "duplicate" | "unknown" | null;
|
||||||
|
|
||||||
const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
||||||
const { t } = useTranslation("domain");
|
const { t } = useTranslation("domain");
|
||||||
const { styles } = useStyles();
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { domains, createDomain, updateDomainById, loading } = useDomainStore();
|
const { domains, createDomain, updateDomainById } = useDomainStore();
|
||||||
|
|
||||||
const isEdit = !!domainId;
|
const isEdit = !!domainId;
|
||||||
const editingDomain = domainId
|
const editingDomain = domainId
|
||||||
? domains.find((d) => d.id === domainId)
|
? domains.find((d) => d.id === domainId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Test connection state
|
||||||
|
const [testing, setTesting] = React.useState(false);
|
||||||
|
const [testResult, setTestResult] = React.useState<TestResult>(null);
|
||||||
|
|
||||||
|
// Submit state (separate from testing)
|
||||||
|
const [submitting, setSubmitting] = React.useState(false);
|
||||||
|
|
||||||
|
// Create error state
|
||||||
|
const [createError, setCreateError] = React.useState<{
|
||||||
|
type: CreateErrorType;
|
||||||
|
message: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Reset form when dialog opens
|
// Reset form when dialog opens
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
// Reset states
|
||||||
|
setTestResult(null);
|
||||||
|
setCreateError(null);
|
||||||
|
setSubmitting(false);
|
||||||
|
setTesting(false);
|
||||||
|
|
||||||
if (editingDomain) {
|
if (editingDomain) {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: editingDomain.name,
|
name: editingDomain.name,
|
||||||
@@ -61,22 +83,156 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
}
|
}
|
||||||
}, [open, editingDomain, form]);
|
}, [open, editingDomain, form]);
|
||||||
|
|
||||||
|
// Clear test result when form values change
|
||||||
|
const handleFieldChange = () => {
|
||||||
|
if (testResult) {
|
||||||
|
setTestResult(null);
|
||||||
|
}
|
||||||
|
if (createError) {
|
||||||
|
setCreateError(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process domain: remove protocol prefix and trailing slashes
|
||||||
|
*/
|
||||||
|
const processDomain = (domain: string): string => {
|
||||||
|
let processed = domain.trim();
|
||||||
|
if (processed.startsWith("https://")) {
|
||||||
|
processed = processed.slice(8);
|
||||||
|
} else if (processed.startsWith("http://")) {
|
||||||
|
processed = processed.slice(7);
|
||||||
|
}
|
||||||
|
return processed.replace(/\/+$/, "");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate domain format
|
||||||
|
*/
|
||||||
|
const validateDomainFormat = (domain: string): boolean => {
|
||||||
|
return /^[\w.-]+$/.test(domain);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get form values for domain connection test
|
||||||
|
*/
|
||||||
|
const getConnectionParams =
|
||||||
|
async (): Promise<TestConnectionParams | null> => {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields([
|
||||||
|
"domain",
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const processedDomain = processDomain(values.domain);
|
||||||
|
|
||||||
|
if (!validateDomainFormat(processedDomain)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
domain: values.domain,
|
||||||
|
username: values.username,
|
||||||
|
password: values.password,
|
||||||
|
processedDomain,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test connection with current form values
|
||||||
|
*/
|
||||||
|
const testConnection = async (
|
||||||
|
params: TestConnectionParams,
|
||||||
|
): Promise<TestResult> => {
|
||||||
|
if (!params) {
|
||||||
|
return { success: false, message: t("validDomainRequired") };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.api.testDomainConnection({
|
||||||
|
domain: params.processedDomain,
|
||||||
|
username: params.username,
|
||||||
|
password: params.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
return { success: true, message: t("connectionSuccess") };
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: result.error || t("connectionFailed"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: error instanceof Error ? error.message : t("connectionFailed"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle test connection button click
|
||||||
|
*/
|
||||||
|
const handleTestConnection = async () => {
|
||||||
|
// Clear previous result
|
||||||
|
setTestResult(null);
|
||||||
|
setCreateError(null);
|
||||||
|
|
||||||
|
const params = await getConnectionParams();
|
||||||
|
if (!params) return;
|
||||||
|
|
||||||
|
setTesting(true);
|
||||||
|
const result = await testConnection(params);
|
||||||
|
setTestResult(result);
|
||||||
|
setTesting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show error message
|
||||||
|
*/
|
||||||
|
const showError = (type: CreateErrorType, message: string) => {
|
||||||
|
setCreateError({
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle form submission
|
||||||
|
*/
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
const values = await form.validateFields();
|
const values = await form.validateFields();
|
||||||
|
const processedDomain = processDomain(values.domain);
|
||||||
// Process domain: remove protocol prefix and trailing slashes
|
|
||||||
let processedDomain = values.domain.trim();
|
|
||||||
if (processedDomain.startsWith("https://")) {
|
|
||||||
processedDomain = processedDomain.slice(8);
|
|
||||||
} else if (processedDomain.startsWith("http://")) {
|
|
||||||
processedDomain = processedDomain.slice(7);
|
|
||||||
}
|
|
||||||
processedDomain = processedDomain.replace(/\/+$/, "");
|
|
||||||
|
|
||||||
// Use domain as name if name is empty
|
|
||||||
const name = values.name?.trim() || processedDomain;
|
const name = values.name?.trim() || processedDomain;
|
||||||
|
|
||||||
|
// Clear previous error
|
||||||
|
setCreateError(null);
|
||||||
|
|
||||||
|
// For new domains, test connection first
|
||||||
|
if (!isEdit) {
|
||||||
|
setSubmitting(true);
|
||||||
|
const testResult = await testConnection({
|
||||||
|
domain: values.domain,
|
||||||
|
username: values.username,
|
||||||
|
password: values.password,
|
||||||
|
processedDomain,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!testResult?.success) {
|
||||||
|
showError("connection", t("createConnectionFailed"));
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSubmitting(true);
|
||||||
|
}
|
||||||
|
|
||||||
if (isEdit && editingDomain) {
|
if (isEdit && editingDomain) {
|
||||||
const params: UpdateDomainParams = {
|
const params: UpdateDomainParams = {
|
||||||
id: domainId,
|
id: domainId,
|
||||||
@@ -85,19 +241,31 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
username: values.username,
|
username: values.username,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include password if provided
|
|
||||||
if (values.password) {
|
if (values.password) {
|
||||||
params.password = values.password;
|
params.password = values.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await updateDomainById(params);
|
const success = await updateDomainById(params);
|
||||||
|
setSubmitting(false);
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success(t("domainUpdated"));
|
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
message.error(t("updateFailed"));
|
showError("unknown", t("updateFailed"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Check for duplicate before creating
|
||||||
|
const existingDomain = domains.find(
|
||||||
|
(d) =>
|
||||||
|
d.domain.toLowerCase() === processedDomain.toLowerCase() &&
|
||||||
|
d.username.toLowerCase() === values.username.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingDomain) {
|
||||||
|
showError("duplicate", t("domainDuplicate"));
|
||||||
|
setSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const params: CreateDomainParams = {
|
const params: CreateDomainParams = {
|
||||||
name,
|
name,
|
||||||
domain: processedDomain,
|
domain: processedDomain,
|
||||||
@@ -106,55 +274,37 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const success = await createDomain(params);
|
const success = await createDomain(params);
|
||||||
|
setSubmitting(false);
|
||||||
if (success) {
|
if (success) {
|
||||||
message.success(t("domainCreated"));
|
|
||||||
onClose();
|
onClose();
|
||||||
} else {
|
} else {
|
||||||
message.error(t("createFailed"));
|
showError("unknown", t("createFailed"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setSubmitting(false);
|
||||||
console.error("Form validation failed:", error);
|
console.error("Form validation failed:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [testing, setTesting] = React.useState(false);
|
/**
|
||||||
|
* Render test button with result icon inside
|
||||||
|
*/
|
||||||
|
const renderTestButton = () => {
|
||||||
|
const getIcon = () => {
|
||||||
|
if (!testResult) return undefined;
|
||||||
|
return testResult.success ? (
|
||||||
|
<CheckCircle2 size={16} color="#52c41a" />
|
||||||
|
) : (
|
||||||
|
<XCircle size={16} color="#ff4d4f" />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Test connection with current form values
|
return (
|
||||||
const handleTestConnection = async () => {
|
<Button onClick={handleTestConnection} loading={testing} icon={getIcon()}>
|
||||||
try {
|
{t("testConnection")}
|
||||||
const values = await form.validateFields([
|
</Button>
|
||||||
"domain",
|
);
|
||||||
"username",
|
|
||||||
"password",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Process domain
|
|
||||||
let processedDomain = values.domain.trim();
|
|
||||||
if (processedDomain.startsWith("https://")) {
|
|
||||||
processedDomain = processedDomain.slice(8);
|
|
||||||
} else if (processedDomain.startsWith("http://")) {
|
|
||||||
processedDomain = processedDomain.slice(7);
|
|
||||||
}
|
|
||||||
processedDomain = processedDomain.replace(/\/+$/, "");
|
|
||||||
|
|
||||||
setTesting(true);
|
|
||||||
const result = await window.api.testDomainConnection({
|
|
||||||
domain: processedDomain,
|
|
||||||
username: values.username,
|
|
||||||
password: values.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
message.success(t("connectionSuccess"));
|
|
||||||
} else {
|
|
||||||
message.error(result.error || t("connectionFailed"));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Test connection failed:", error);
|
|
||||||
} finally {
|
|
||||||
setTesting(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,7 +317,7 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
mask={{ closable: false }}
|
mask={{ closable: false }}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" className={styles.form}>
|
<Form form={form} layout="vertical" onValuesChange={handleFieldChange}>
|
||||||
<Form.Item name="name" label={t("name")} style={{ marginTop: 8 }}>
|
<Form.Item name="name" label={t("name")} style={{ marginTop: 8 }}>
|
||||||
<Input placeholder={t("nameOptional")} />
|
<Input placeholder={t("nameOptional")} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -180,17 +330,8 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
{
|
{
|
||||||
validator: (_, value) => {
|
validator: (_, value) => {
|
||||||
if (!value) return Promise.resolve();
|
if (!value) return Promise.resolve();
|
||||||
// Allow https:// or http:// prefix
|
const processed = processDomain(value);
|
||||||
let domain = value.trim();
|
if (validateDomainFormat(processed)) {
|
||||||
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)) {
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject(new Error(t("validDomainRequired")));
|
return Promise.reject(new Error(t("validDomainRequired")));
|
||||||
@@ -212,21 +353,42 @@ const DomainForm: React.FC<DomainFormProps> = ({ open, onClose, domainId }) => {
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
name="password"
|
name="password"
|
||||||
label={t("password")}
|
label={t("password")}
|
||||||
rules={isEdit ? [] : [{ required: true, message: t("enterPassword") }]}
|
rules={
|
||||||
|
isEdit ? [] : [{ required: true, message: t("enterPassword") }]
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<InputPassword
|
||||||
placeholder={isEdit ? t("keepPasswordHint") : t("enterPassword")}
|
placeholder={isEdit ? t("keepPasswordHint") : t("enterPassword")}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item style={{ marginTop: 24, marginBottom: 0 }}>
|
<Form.Item style={{ marginTop: 32, marginBottom: 0 }}>
|
||||||
<div style={{ display: "flex", justifyContent: "space-between", gap: 8 }}>
|
<div
|
||||||
<Button onClick={handleTestConnection} loading={testing}>
|
style={{
|
||||||
{t("testConnection")}
|
display: "flex",
|
||||||
</Button>
|
justifyContent: "space-between",
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
gap: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Left side: Cancel button and error message */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
<Button onClick={onClose}>{t("cancel", { ns: "common" })}</Button>
|
<Button onClick={onClose}>{t("cancel", { ns: "common" })}</Button>
|
||||||
<Button type="primary" onClick={handleSubmit} loading={loading}>
|
</div>
|
||||||
|
|
||||||
|
{/* Right side: Test button and Create/Update button */}
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
|
{createError && (
|
||||||
|
<span style={{ color: "#ff4d4f", fontSize: 14 }}>
|
||||||
|
{createError.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{renderTestButton()}
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
loading={submitting}
|
||||||
|
>
|
||||||
{isEdit ? t("update") : t("create")}
|
{isEdit ? t("update") : t("create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,33 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* DomainList Component
|
* 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 React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Tag, Popconfirm, Space } from "antd";
|
import { Popconfirm, Space } from "antd";
|
||||||
import { Button, Tooltip } from "@lobehub/ui";
|
import { Button, SortableList, Tooltip } from "@lobehub/ui";
|
||||||
import { Avatar } from "@lobehub/ui";
|
import { Pencil, Trash2 } from "lucide-react";
|
||||||
import {
|
|
||||||
Cloud,
|
|
||||||
Pencil,
|
|
||||||
Trash2,
|
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
HelpCircle,
|
|
||||||
GripVertical,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { createStyles } from "antd-style";
|
import { createStyles } from "antd-style";
|
||||||
import { useDomainStore, useUIStore } from "@renderer/stores";
|
import { useDomainStore } from "@renderer/stores";
|
||||||
import type { Domain } from "@shared/types/domain";
|
import type { Domain } from "@shared/types/domain";
|
||||||
|
|
||||||
const useStyles = createStyles(({ token, css }) => ({
|
const useStyles = createStyles(({ token, css }) => ({
|
||||||
item: css`
|
itemWrapper: css`
|
||||||
padding: ${token.paddingSM}px;
|
width: 100%;
|
||||||
|
padding: ${token.paddingXS}px ${token.paddingSM}px;
|
||||||
border-radius: ${token.borderRadiusLG}px;
|
border-radius: ${token.borderRadiusLG}px;
|
||||||
background: ${token.colorBgContainer};
|
background: ${token.colorBgContainer};
|
||||||
border: 1px solid ${token.colorBorderSecondary};
|
border: 1px solid ${token.colorBorderSecondary};
|
||||||
margin-bottom: ${token.marginXS}px;
|
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
@@ -35,15 +25,17 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
border-color: ${token.colorPrimary};
|
border-color: ${token.colorPrimary};
|
||||||
box-shadow: ${token.boxShadowSecondary};
|
box-shadow: ${token.boxShadowSecondary};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover .domain-item-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
selectedItem: css`
|
selectedItem: css`
|
||||||
border-color: ${token.colorPrimary};
|
border-color: ${token.colorPrimary};
|
||||||
background: ${token.colorPrimaryBg};
|
background: ${token.colorPrimaryBg};
|
||||||
`,
|
`,
|
||||||
itemDragging: css`
|
|
||||||
opacity: 0.5;
|
|
||||||
border: 2px dashed ${token.colorPrimary};
|
|
||||||
`,
|
|
||||||
domainInfo: css`
|
domainInfo: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -51,6 +43,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
domainName: css`
|
domainName: css`
|
||||||
font-weight: ${token.fontWeightStrong};
|
font-weight: ${token.fontWeightStrong};
|
||||||
font-size: ${token.fontSizeLG}px;
|
font-size: ${token.fontSizeLG}px;
|
||||||
@@ -58,6 +51,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
domainUrl: css`
|
domainUrl: css`
|
||||||
color: ${token.colorTextSecondary};
|
color: ${token.colorTextSecondary};
|
||||||
font-size: ${token.fontSizeSM}px;
|
font-size: ${token.fontSizeSM}px;
|
||||||
@@ -65,33 +59,29 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
actions: css`
|
actions: css`
|
||||||
|
position: absolute;
|
||||||
|
right: ${token.paddingXS}px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: ${token.paddingXS}px;
|
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`
|
itemContent: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: ${token.paddingSM}px;
|
gap: ${token.paddingXS}px;
|
||||||
|
position: relative;
|
||||||
`,
|
`,
|
||||||
|
|
||||||
domainText: css`
|
domainText: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -106,17 +96,8 @@ interface DomainListProps {
|
|||||||
const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
|
const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
|
||||||
const { t } = useTranslation("domain");
|
const { t } = useTranslation("domain");
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
const {
|
const { domains, currentDomain, switchDomain, deleteDomain, reorderDomains } =
|
||||||
domains,
|
useDomainStore();
|
||||||
currentDomain,
|
|
||||||
connectionStatuses,
|
|
||||||
switchDomain,
|
|
||||||
deleteDomain,
|
|
||||||
testConnection,
|
|
||||||
reorderDomains,
|
|
||||||
} = useDomainStore();
|
|
||||||
const { domainIconColors } = useUIStore();
|
|
||||||
const [draggingIndex, setDraggingIndex] = React.useState<number | null>(null);
|
|
||||||
|
|
||||||
const handleSelect = (domain: Domain) => {
|
const handleSelect = (domain: Domain) => {
|
||||||
switchDomain(domain);
|
switchDomain(domain);
|
||||||
@@ -126,109 +107,46 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
|
|||||||
await deleteDomain(id);
|
await deleteDomain(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusIcon = (id: string) => {
|
// Handle reorder - convert SortableListItem[] back to reorder action
|
||||||
const status = connectionStatuses[id];
|
const handleSortChange = (newItems: { id: string }[]) => {
|
||||||
switch (status) {
|
const newOrder = newItems.map((item) => item.id);
|
||||||
case "connected":
|
const oldOrder = domains.map((d) => d.id);
|
||||||
return <CheckCircle size={16} style={{ color: "#52c41a" }} />;
|
|
||||||
case "error":
|
// Find the from and to indices
|
||||||
return <XCircle size={16} style={{ color: "#ff4d4f" }} />;
|
for (let i = 0; i < oldOrder.length; i++) {
|
||||||
default:
|
if (oldOrder[i] !== newOrder[i]) {
|
||||||
return <HelpCircle size={16} style={{ color: "#faad14" }} />;
|
const fromIndex = i;
|
||||||
|
const toIndex = newOrder.indexOf(oldOrder[i]);
|
||||||
|
reorderDomains(fromIndex, toIndex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusTag = (id: string) => {
|
const renderItem = (item: { id: string }) => {
|
||||||
const status = connectionStatuses[id];
|
const domain = domains.find((d) => d.id === item.id);
|
||||||
switch (status) {
|
if (!domain) return null;
|
||||||
case "connected":
|
|
||||||
return <Tag color="success">{t("connected")}</Tag>;
|
|
||||||
case "error":
|
|
||||||
return <Tag color="error">{t("connectionFailed")}</Tag>;
|
|
||||||
default:
|
|
||||||
return <Tag color="warning">{t("notTested")}</Tag>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getDomainIconColor = (domainId: string, isSelected: boolean) => {
|
|
||||||
if (domainIconColors[domainId]) {
|
|
||||||
return domainIconColors[domainId];
|
|
||||||
}
|
|
||||||
return isSelected ? "#1890ff" : "#87d068";
|
|
||||||
};
|
|
||||||
|
|
||||||
// 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 isSelected = currentDomain?.id === domain.id;
|
||||||
const isDragging = draggingIndex === index;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SortableList.Item id={domain.id}>
|
||||||
<div
|
<div
|
||||||
key={domain.id}
|
className={`${styles.itemWrapper} ${isSelected ? styles.selectedItem : ""}`}
|
||||||
className={`${styles.item} ${isSelected ? styles.selectedItem : ""} ${isDragging ? styles.itemDragging : ""}`}
|
|
||||||
onClick={() => handleSelect(domain)}
|
onClick={() => handleSelect(domain)}
|
||||||
draggable
|
|
||||||
onDragStart={() => handleDragStart(index)}
|
|
||||||
onDragOver={(e) => handleDragOver(e, index)}
|
|
||||||
onDrop={(e) => handleDrop(e, index)}
|
|
||||||
onDragEnd={handleDragEnd}
|
|
||||||
>
|
>
|
||||||
<div className={styles.itemContent}>
|
<div className={styles.itemContent}>
|
||||||
<div className={styles.dragHandle}>
|
<SortableList.DragHandle />
|
||||||
<GripVertical size={16} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.domainInfo}>
|
<div className={styles.domainInfo}>
|
||||||
<Avatar
|
|
||||||
icon={<Cloud size={16} />}
|
|
||||||
style={{
|
|
||||||
backgroundColor: getDomainIconColor(domain.id, isSelected),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className={styles.domainText}>
|
<div className={styles.domainText}>
|
||||||
<div className={styles.domainName}>
|
<div className={styles.domainName}>{domain.name}</div>
|
||||||
{domain.name}
|
|
||||||
{getStatusTag(domain.id)}
|
|
||||||
</div>
|
|
||||||
<div className={styles.domainUrl}>
|
<div className={styles.domainUrl}>
|
||||||
{domain.domain} · {domain.username}
|
{domain.username} · {domain.domain}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space>
|
<Space className={"domain-item-actions " + styles.actions}>
|
||||||
<Tooltip title={t("testConnection")}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
icon={getStatusIcon(domain.id)}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
testConnection(domain.id);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={t("edit")}>
|
<Tooltip title={t("edit")}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
@@ -251,6 +169,7 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
|
|||||||
okText={t("delete", { ns: "common" })}
|
okText={t("delete", { ns: "common" })}
|
||||||
cancelText={t("cancel", { ns: "common" })}
|
cancelText={t("cancel", { ns: "common" })}
|
||||||
>
|
>
|
||||||
|
<Tooltip title={t("delete", { ns: "common" })}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -258,13 +177,22 @@ const DomainList: React.FC<DomainListProps> = ({ onEdit }) => {
|
|||||||
icon={<Trash2 size={16} />}
|
icon={<Trash2 size={16} />}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</SortableList.Item>
|
||||||
);
|
);
|
||||||
})}
|
};
|
||||||
</>
|
|
||||||
|
return (
|
||||||
|
<SortableList
|
||||||
|
items={domains}
|
||||||
|
renderItem={renderItem}
|
||||||
|
onChange={handleSortChange}
|
||||||
|
gap={4}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,10 +38,9 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: ${token.marginLG}px;
|
margin-bottom: ${token.marginXXS}px;
|
||||||
padding: 0 ${token.paddingLG}px;
|
padding: 0 ${token.paddingLG}px;
|
||||||
padding-top: ${token.paddingLG}px;
|
padding-top: ${token.paddingXS}px;
|
||||||
border-bottom: 1px solid ${token.colorBorderSecondary};
|
|
||||||
`,
|
`,
|
||||||
title: css`
|
title: css`
|
||||||
font-size: ${token.fontSizeHeading4}px;
|
font-size: ${token.fontSizeHeading4}px;
|
||||||
@@ -56,7 +55,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|||||||
content: css`
|
content: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 0 ${token.paddingLG}px;
|
padding: 0 ${token.paddingSM}px;
|
||||||
`,
|
`,
|
||||||
loading: css`
|
loading: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -194,7 +193,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
{currentDomain.name}
|
{currentDomain.name}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.collapsedUrl}>
|
<div className={styles.collapsedUrl}>
|
||||||
{currentDomain.domain}
|
{currentDomain.username} · {currentDomain.domain}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -256,11 +255,7 @@ const DomainManager: React.FC<DomainManagerProps> = ({
|
|||||||
) : domains.length === 0 ? (
|
) : domains.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description={t("noDomainConfig")}
|
description={t("noDomainConfig")}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
>
|
>
|
||||||
<Button type="primary" onClick={handleAdd}>
|
|
||||||
{t("addFirstDomainConfig")}
|
|
||||||
</Button>
|
|
||||||
</Empty>
|
</Empty>
|
||||||
) : (
|
) : (
|
||||||
<DomainList onEdit={handleEdit} />
|
<DomainList onEdit={handleEdit} />
|
||||||
|
|||||||
@@ -194,7 +194,6 @@ const VersionHistory: React.FC = () => {
|
|||||||
<div style={{ padding: 24, textAlign: "center" }}>
|
<div style={{ padding: 24, textAlign: "center" }}>
|
||||||
<Empty
|
<Empty
|
||||||
description={t("selectApp", { ns: "app" })}
|
description={t("selectApp", { ns: "app" })}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +229,6 @@ const VersionHistory: React.FC = () => {
|
|||||||
{versions.length === 0 ? (
|
{versions.length === 0 ? (
|
||||||
<Empty
|
<Empty
|
||||||
description={t("noVersions")}
|
description={t("noVersions")}
|
||||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<List
|
<List
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"domainManagement": "Domain Management",
|
"domainManagement": "Domain Management",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"noDomainConfig": "No Domain configuration",
|
"noDomainConfig": "No Domain configuration",
|
||||||
"addFirstDomainConfig": "Add first Domain",
|
|
||||||
"noDomainSelected": "No Domain selected",
|
"noDomainSelected": "No Domain selected",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"testConnection": "Test Connection",
|
"testConnection": "Test Connection",
|
||||||
@@ -55,6 +54,8 @@
|
|||||||
"update": "Update",
|
"update": "Update",
|
||||||
"domainCreated": "Domain created successfully",
|
"domainCreated": "Domain created successfully",
|
||||||
"domainUpdated": "Domain updated successfully",
|
"domainUpdated": "Domain updated successfully",
|
||||||
|
"createConnectionFailed": "Connection failed during creation",
|
||||||
|
"domainDuplicate": "This domain and username combination already exists",
|
||||||
"createFailed": "Creation failed",
|
"createFailed": "Creation failed",
|
||||||
"updateFailed": "Update failed"
|
"updateFailed": "Update failed"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"domainManagement": "ドメイン管理",
|
"domainManagement": "ドメイン管理",
|
||||||
"add": "追加",
|
"add": "追加",
|
||||||
"noDomainConfig": "ドメイン設定がありません",
|
"noDomainConfig": "ドメイン設定がありません",
|
||||||
"addFirstDomainConfig": "最初のドメインを追加",
|
|
||||||
"noDomainSelected": "ドメイン未選択",
|
"noDomainSelected": "ドメイン未選択",
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
"testConnection": "接続テスト",
|
"testConnection": "接続テスト",
|
||||||
@@ -55,6 +54,8 @@
|
|||||||
"update": "更新",
|
"update": "更新",
|
||||||
"domainCreated": "ドメインを作成しました",
|
"domainCreated": "ドメインを作成しました",
|
||||||
"domainUpdated": "ドメインを更新しました",
|
"domainUpdated": "ドメインを更新しました",
|
||||||
|
"createConnectionFailed": "接続に失敗しました",
|
||||||
|
"domainDuplicate": "このドメインとユーザー名の組み合わせは既に存在します",
|
||||||
"createFailed": "作成に失敗しました",
|
"createFailed": "作成に失敗しました",
|
||||||
"updateFailed": "更新に失敗しました"
|
"updateFailed": "更新に失敗しました"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@
|
|||||||
"domainManagement": "Domain 管理",
|
"domainManagement": "Domain 管理",
|
||||||
"add": "添加",
|
"add": "添加",
|
||||||
"noDomainConfig": "暂无 Domain 配置",
|
"noDomainConfig": "暂无 Domain 配置",
|
||||||
"addFirstDomainConfig": "添加第一个 Domain",
|
|
||||||
"noDomainSelected": "未选择 Domain",
|
"noDomainSelected": "未选择 Domain",
|
||||||
"edit": "编辑",
|
"edit": "编辑",
|
||||||
"testConnection": "测试连接",
|
"testConnection": "测试连接",
|
||||||
@@ -55,6 +54,8 @@
|
|||||||
"update": "更新",
|
"update": "更新",
|
||||||
"domainCreated": "Domain 创建成功",
|
"domainCreated": "Domain 创建成功",
|
||||||
"domainUpdated": "Domain 更新成功",
|
"domainUpdated": "Domain 更新成功",
|
||||||
|
"createConnectionFailed": "创建时连接失败",
|
||||||
|
"domainDuplicate": "该域名和用户名组合已存在",
|
||||||
"createFailed": "创建失败",
|
"createFailed": "创建失败",
|
||||||
"updateFailed": "更新失败"
|
"updateFailed": "更新失败"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user