接入可配置文档编辑与智能取数服务
This commit is contained in:
@@ -185,6 +185,12 @@ eco-market:
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
|
||||
|
||||
intelligent-data:
|
||||
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
|
||||
|
||||
document-editor:
|
||||
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
|
||||
@@ -185,6 +185,12 @@ eco-market:
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:18020}
|
||||
|
||||
intelligent-data:
|
||||
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
|
||||
|
||||
document-editor:
|
||||
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
|
||||
@@ -182,6 +182,12 @@ eco-market:
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
|
||||
|
||||
intelligent-data:
|
||||
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
|
||||
|
||||
document-editor:
|
||||
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
|
||||
@@ -182,6 +182,12 @@ eco-market:
|
||||
mcp:
|
||||
proxy-base-url: ${MCP_PROXY_URL:http://localhost:8020}
|
||||
|
||||
intelligent-data:
|
||||
service-url: ${INTELLIGENT_DATA_SERVICE_URL:http://192.168.2.106:3000/test-console}
|
||||
|
||||
document-editor:
|
||||
service-url: ${DOCUMENT_EDITOR_SERVICE_URL:}
|
||||
|
||||
knife4j:
|
||||
# 开启增强配置
|
||||
enable: true
|
||||
|
||||
@@ -179,6 +179,12 @@ public class TenantConfigDto implements Serializable {
|
||||
@Schema(description = "模型API根地址")
|
||||
private String baseModelApiUrl;
|
||||
|
||||
@Schema(description = "智能取数服务地址")
|
||||
private String intelligentDataServiceUrl;
|
||||
|
||||
@Schema(description = "文档编辑服务地址")
|
||||
private String documentEditorServiceUrl;
|
||||
|
||||
public enum AuthTypeEnum {
|
||||
PHONE(1, "手机"),
|
||||
CAS(2, "CAS"),
|
||||
|
||||
@@ -72,6 +72,30 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
|
||||
.required(true)
|
||||
.sort(1)
|
||||
.build());
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("intelligentDataServiceUrl")
|
||||
.description("智能取数服务地址")
|
||||
.value("http://192.168.2.106:3000/test-console")
|
||||
.notice("客户端点击智能取数时加载的 HTTP 或 HTTPS 服务地址")
|
||||
.placeholder("请输入智能取数服务地址")
|
||||
.category(TenantConfig.ConfigCategory.BaseConfig)
|
||||
.inputType(TenantConfig.InputType.Input)
|
||||
.dataType(TenantConfig.DataType.String)
|
||||
.required(true)
|
||||
.sort(2)
|
||||
.build());
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("documentEditorServiceUrl")
|
||||
.description("文档编辑服务地址")
|
||||
.value("")
|
||||
.notice("客户端点击文档编辑时加载的 HTTP 或 HTTPS 服务地址")
|
||||
.placeholder("请输入文档编辑服务地址")
|
||||
.category(TenantConfig.ConfigCategory.BaseConfig)
|
||||
.inputType(TenantConfig.InputType.Input)
|
||||
.dataType(TenantConfig.DataType.String)
|
||||
.required(false)
|
||||
.sort(3)
|
||||
.build());
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("siteDescription")
|
||||
.description("站点介绍")
|
||||
|
||||
@@ -38,6 +38,12 @@ public class TenantController {
|
||||
@Value("${model-api-proxy.base-api-url:}")
|
||||
private String baseApiUrl;
|
||||
|
||||
@Value("${intelligent-data.service-url:http://192.168.2.106:3000/test-console}")
|
||||
private String intelligentDataServiceUrl;
|
||||
|
||||
@Value("${document-editor.service-url:}")
|
||||
private String documentEditorServiceUrl;
|
||||
|
||||
@Operation(summary = "租户配置信息查询接口")
|
||||
@RequestMapping(path = "/config", method = RequestMethod.GET)
|
||||
public ReqResult<TenantConfigDto> getConfig() {
|
||||
@@ -69,6 +75,12 @@ public class TenantController {
|
||||
tenantConfigDto.setFaviconUrl(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getFaviconUrl(), true));
|
||||
tenantConfigDto.setSquareBanner(iFileAccessService.getFileUrlWithAk(tenantConfigDto.getSquareBanner(), true));
|
||||
tenantConfigDto.setMaxFileSize(maxFileSize);
|
||||
if (StringUtils.isBlank(tenantConfigDto.getIntelligentDataServiceUrl())) {
|
||||
tenantConfigDto.setIntelligentDataServiceUrl(intelligentDataServiceUrl);
|
||||
}
|
||||
if (StringUtils.isBlank(tenantConfigDto.getDocumentEditorServiceUrl())) {
|
||||
tenantConfigDto.setDocumentEditorServiceUrl(documentEditorServiceUrl);
|
||||
}
|
||||
if (StringUtils.isBlank(tenantConfigDto.getTemplateConfig()) || tenantConfigDto.getTemplateConfig().equals("{}")) {
|
||||
tenantConfigDto.setTemplateConfig("{\"primaryColor\":\"#5147ff\",\"backgroundId\":\"bg-variant-1\",\"antdTheme\":\"light\",\"layoutStyle\":\"light\",\"navigationStyle\":\"style1\",\"timestamp\":1757425328082}");
|
||||
}
|
||||
@@ -108,4 +120,4 @@ public class TenantController {
|
||||
I18nUtil.replaceSystemMessage(tenantConfigDto);
|
||||
return ReqResult.success(tenantConfigDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +126,24 @@ export default function BaseFormItem({
|
||||
},
|
||||
],
|
||||
};
|
||||
const serviceUrlFieldNames = [
|
||||
'intelligentDataServiceUrl',
|
||||
'documentEditorServiceUrl',
|
||||
];
|
||||
const fieldRules: Rule[] | undefined = serviceUrlFieldNames.includes(
|
||||
props.name,
|
||||
)
|
||||
? [
|
||||
{
|
||||
required: props.name === 'intelligentDataServiceUrl',
|
||||
message: `请输入${props.description}`,
|
||||
},
|
||||
{
|
||||
type: 'url',
|
||||
message: '请输入有效的 HTTP 或 HTTPS 地址',
|
||||
},
|
||||
]
|
||||
: rules[currentTab];
|
||||
// 处理 Select 类型的初始值:-1 表示未选择,应转换为 undefined
|
||||
const getInitialValue = () => {
|
||||
if (['Select'].includes(props.inputType) && Number(props.value) === -1) {
|
||||
@@ -181,7 +199,7 @@ export default function BaseFormItem({
|
||||
key={props.name}
|
||||
name={props.name}
|
||||
initialValue={getInitialValue()}
|
||||
rules={rules[currentTab]}
|
||||
rules={fieldRules}
|
||||
{...(props.inputType === 'MultiInput'
|
||||
? {
|
||||
label: <MultiInputLabel />,
|
||||
|
||||
@@ -369,6 +369,10 @@ export interface TenantConfigDto {
|
||||
siteName?: string;
|
||||
/** 站点描述 */
|
||||
siteDescription?: string;
|
||||
/** 智能取数服务地址 */
|
||||
intelligentDataServiceUrl?: string;
|
||||
/** 文档编辑服务地址 */
|
||||
documentEditorServiceUrl?: string;
|
||||
/** 站点LOGO(为空使用默认值) */
|
||||
siteLogo?: string;
|
||||
/** 登录页banner */
|
||||
|
||||
@@ -50,6 +50,8 @@ import SkillsMarketplacePage from "./components/pages/SkillsMarketplacePage";
|
||||
import AgentMarketplacePage from "./components/pages/AgentMarketplacePage";
|
||||
import McpMarketplacePage from "./components/pages/McpMarketplacePage";
|
||||
import WorkflowMarketplacePage from "./components/pages/WorkflowMarketplacePage";
|
||||
import DataFetchPage from "./components/pages/DataFetchPage";
|
||||
import DocumentEditorPage from "./components/pages/DocumentEditorPage";
|
||||
import MCPSettings from "./components/settings/MCPSettings";
|
||||
import AppSidebar, {
|
||||
NavKey,
|
||||
@@ -106,6 +108,8 @@ type TabKey =
|
||||
| "legacySessions"
|
||||
| "automation"
|
||||
| "knowledge"
|
||||
| "document"
|
||||
| "dataFetch"
|
||||
| "skills"
|
||||
| "agents"
|
||||
| "mcpLibrary"
|
||||
@@ -1080,6 +1084,16 @@ function App() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "document") {
|
||||
setActiveTab("document");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "dataFetch") {
|
||||
setActiveTab("dataFetch");
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "toolIntegration" || key === "systemSettings") {
|
||||
setActiveTab("settings");
|
||||
return;
|
||||
@@ -1337,7 +1351,9 @@ function App() {
|
||||
{/* 主内容区:flex 子撑满,便于日志等页占满高度 */}
|
||||
<div
|
||||
className={
|
||||
webviewActions
|
||||
webviewActions ||
|
||||
activeTab === "document" ||
|
||||
activeTab === "dataFetch"
|
||||
? "app-content app-content-fullwidth"
|
||||
: "app-content"
|
||||
}
|
||||
@@ -1385,6 +1401,8 @@ function App() {
|
||||
)}
|
||||
{activeTab === "automation" && <AutomationPage />}
|
||||
{activeTab === "knowledge" && <KnowledgePage />}
|
||||
{activeTab === "document" && <DocumentEditorPage />}
|
||||
{activeTab === "dataFetch" && <DataFetchPage />}
|
||||
{activeTab === "skills" && <SkillsMarketplacePage />}
|
||||
{activeTab === "agents" && (
|
||||
<AgentMarketplacePage onStartChat={handleStartAgentChat} />
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Alert, Button, Spin } from "antd";
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import styles from "../../styles/components/ConfiguredServicePage.module.css";
|
||||
|
||||
interface ConfiguredServicePageProps {
|
||||
title: string;
|
||||
loadServiceUrl: () => Promise<string>;
|
||||
}
|
||||
|
||||
function ConfiguredServicePage({
|
||||
title,
|
||||
loadServiceUrl,
|
||||
}: ConfiguredServicePageProps) {
|
||||
const webviewRef = useRef<HTMLElement | null>(null);
|
||||
const [serviceUrl, setServiceUrl] = useState<string | null>(null);
|
||||
const [loadingConfig, setLoadingConfig] = useState(true);
|
||||
const [loadingPage, setLoadingPage] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadService = useCallback(async () => {
|
||||
setLoadingConfig(true);
|
||||
setError(null);
|
||||
try {
|
||||
setServiceUrl(await loadServiceUrl());
|
||||
} catch (cause) {
|
||||
setServiceUrl(null);
|
||||
setError(
|
||||
cause instanceof Error ? cause.message : `读取${title}服务配置失败`,
|
||||
);
|
||||
} finally {
|
||||
setLoadingConfig(false);
|
||||
}
|
||||
}, [loadServiceUrl, title]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadService();
|
||||
}, [loadService]);
|
||||
|
||||
useEffect(() => {
|
||||
const element = webviewRef.current as any;
|
||||
if (!element || !serviceUrl) return;
|
||||
|
||||
const handleStart = () => {
|
||||
setLoadingPage(true);
|
||||
setError(null);
|
||||
};
|
||||
const handleStop = () => setLoadingPage(false);
|
||||
const handleFailure = (event: any) => {
|
||||
if (event.errorCode !== -3) {
|
||||
setLoadingPage(false);
|
||||
setError(
|
||||
`${title}页面加载失败:${event.errorDescription || "网络连接失败"}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
element.addEventListener("did-start-loading", handleStart);
|
||||
element.addEventListener("did-stop-loading", handleStop);
|
||||
element.addEventListener("did-fail-load", handleFailure);
|
||||
return () => {
|
||||
element.removeEventListener("did-start-loading", handleStart);
|
||||
element.removeEventListener("did-stop-loading", handleStop);
|
||||
element.removeEventListener("did-fail-load", handleFailure);
|
||||
};
|
||||
}, [serviceUrl, title]);
|
||||
|
||||
const handleReload = useCallback(() => {
|
||||
const element = webviewRef.current as any;
|
||||
if (element?.reload) {
|
||||
element.reload();
|
||||
return;
|
||||
}
|
||||
void loadService();
|
||||
}, [loadService]);
|
||||
|
||||
if (loadingConfig) {
|
||||
return (
|
||||
<div className={styles.state}>
|
||||
<Spin tip={`正在加载${title}`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!serviceUrl) {
|
||||
return (
|
||||
<div className={styles.state}>
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={error || `${title}服务地址未配置`}
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={loadService}
|
||||
>
|
||||
重试
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.toolbar}>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<span className={styles.url} title={serviceUrl}>
|
||||
{serviceUrl}
|
||||
</span>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={loadingPage}
|
||||
onClick={handleReload}
|
||||
title={`刷新${title}页面`}
|
||||
>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
{error && (
|
||||
<Alert
|
||||
className={styles.alert}
|
||||
type="error"
|
||||
showIcon
|
||||
closable
|
||||
message={error}
|
||||
onClose={() => setError(null)}
|
||||
/>
|
||||
)}
|
||||
<webview
|
||||
key={serviceUrl}
|
||||
ref={webviewRef as any}
|
||||
src={serviceUrl}
|
||||
allowpopups={"true" as any}
|
||||
className={styles.webview}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfiguredServicePage;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { fetchIntelligentDataServiceUrl } from "../../services/core/servicePages";
|
||||
import ConfiguredServicePage from "./ConfiguredServicePage";
|
||||
|
||||
function DataFetchPage() {
|
||||
return (
|
||||
<ConfiguredServicePage
|
||||
title="智能取数"
|
||||
loadServiceUrl={fetchIntelligentDataServiceUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataFetchPage;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from "react";
|
||||
import { fetchDocumentEditorServiceUrl } from "../../services/core/servicePages";
|
||||
import ConfiguredServicePage from "./ConfiguredServicePage";
|
||||
|
||||
function DocumentEditorPage() {
|
||||
return (
|
||||
<ConfiguredServicePage
|
||||
title="文档编辑"
|
||||
loadServiceUrl={fetchDocumentEditorServiceUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DocumentEditorPage;
|
||||
@@ -2,7 +2,7 @@ import { message } from "antd";
|
||||
import {
|
||||
AUTH_KEYS,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
DEFAULT_SERVER_HOST,
|
||||
STORAGE_KEYS,
|
||||
} from "@shared/constants";
|
||||
import { getDomainTokenKey } from "@shared/utils/domain";
|
||||
import { normalizeServerHost } from "./auth";
|
||||
@@ -179,11 +179,15 @@ export interface AutomationTemplate {
|
||||
}
|
||||
|
||||
async function getBaseUrl(): Promise<string> {
|
||||
const config = (await window.electronAPI?.settings.get(
|
||||
AUTH_KEYS.STEP1_CONFIG,
|
||||
)) as { serverHost?: string } | null;
|
||||
|
||||
return normalizeServerHost(config?.serverHost || DEFAULT_SERVER_HOST);
|
||||
const [userInfo, config] = (await Promise.all([
|
||||
window.electronAPI?.settings.get(AUTH_KEYS.USER_INFO),
|
||||
window.electronAPI?.settings.get(STORAGE_KEYS.STEP1_CONFIG),
|
||||
])) as [{ currentDomain?: string } | null, { serverHost?: string } | null];
|
||||
const loginServerHost = userInfo?.currentDomain || config?.serverHost || "";
|
||||
if (!loginServerHost) {
|
||||
throw new Error("未找到登录站点地址,请重新登录");
|
||||
}
|
||||
return normalizeServerHost(loginServerHost);
|
||||
}
|
||||
|
||||
async function getAuthToken(domain: string): Promise<string | null> {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
AUTH_KEYS,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
STORAGE_KEYS,
|
||||
} from "@shared/constants";
|
||||
import { getDomainTokenKey } from "@shared/utils/domain";
|
||||
import { normalizeServerHost } from "./auth";
|
||||
|
||||
const SUCCESS_CODE = "0000";
|
||||
const INTELLIGENT_DATA_FALLBACK_URL = "http://192.168.2.106:3000/test-console";
|
||||
|
||||
interface TenantConfigResponse {
|
||||
code: string;
|
||||
message?: string;
|
||||
data?: {
|
||||
intelligentDataServiceUrl?: string;
|
||||
documentEditorServiceUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
async function getLoginServerHost(): Promise<string> {
|
||||
const [userInfo, config] = (await Promise.all([
|
||||
window.electronAPI?.settings.get(AUTH_KEYS.USER_INFO),
|
||||
window.electronAPI?.settings.get(STORAGE_KEYS.STEP1_CONFIG),
|
||||
])) as [{ currentDomain?: string } | null, { serverHost?: string } | null];
|
||||
const loginServerHost = userInfo?.currentDomain || config?.serverHost || "";
|
||||
if (!loginServerHost) {
|
||||
throw new Error("未找到登录站点地址,请重新登录");
|
||||
}
|
||||
return normalizeServerHost(loginServerHost);
|
||||
}
|
||||
|
||||
async function getToken(serverHost: string): Promise<string | null> {
|
||||
const oneShotToken = (await window.electronAPI?.settings.get(
|
||||
AUTH_KEYS.AUTH_TOKEN,
|
||||
)) as string | null;
|
||||
if (oneShotToken) return oneShotToken;
|
||||
return (await window.electronAPI?.settings.get(
|
||||
getDomainTokenKey(serverHost),
|
||||
)) as string | null;
|
||||
}
|
||||
|
||||
function normalizeServiceUrl(value: string, serviceName: string): string {
|
||||
const candidate = value.trim();
|
||||
if (!candidate) {
|
||||
throw new Error(`${serviceName}服务地址未配置,请在系统设置中配置`);
|
||||
}
|
||||
const url = new URL(candidate);
|
||||
if (!/^https?:$/.test(url.protocol)) {
|
||||
throw new Error(`${serviceName}服务地址必须使用 HTTP 或 HTTPS`);
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
async function fetchTenantConfig(): Promise<TenantConfigResponse["data"]> {
|
||||
const serverHost = await getLoginServerHost();
|
||||
const token = await getToken(serverHost);
|
||||
const response = await fetch(`${serverHost}/api/tenant/config`, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(DEFAULT_API_TIMEOUT),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`读取服务配置失败(HTTP ${response.status})`);
|
||||
}
|
||||
|
||||
const result = (await response.json()) as TenantConfigResponse;
|
||||
if (result.code !== SUCCESS_CODE) {
|
||||
throw new Error(result.message || "读取服务配置失败");
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function fetchIntelligentDataServiceUrl(): Promise<string> {
|
||||
const config = await fetchTenantConfig();
|
||||
return normalizeServiceUrl(
|
||||
config?.intelligentDataServiceUrl || INTELLIGENT_DATA_FALLBACK_URL,
|
||||
"智能取数",
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetchDocumentEditorServiceUrl(): Promise<string> {
|
||||
const config = await fetchTenantConfig();
|
||||
return normalizeServiceUrl(
|
||||
config?.documentEditorServiceUrl || "",
|
||||
"文档编辑",
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-container);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
padding: 7px 16px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: var(--color-bg-section-header);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.alert {
|
||||
margin: 8px 12px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.webview {
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
border: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
min-height: 240px;
|
||||
padding: 24px;
|
||||
}
|
||||
Reference in New Issue
Block a user