🎨 style(connection): 连接表单必填项置顶并去掉层层卡片

信息架构:原顺序是 身份信息 → 连接 URI → …(约 800 行后)→ 目标地址 → 服务与数据库 → 凭据,
最关键的 host/端口/账号/密码排在最后,而可选的连接名称、预设类型与很高的 URI 多行输入框
占据首屏。

- 把 identity 与 uri 两组从条件链之前搬到之后,使首屏依次为目标地址 → 生产连接保护 →
  连接模式 → 认证凭据;连接名称与预设类型下沉,URI 作为「粘贴整串」的高级入口置于末尾
- URI 分组改为可折叠且默认折叠;编辑已含 uri 的连接时默认展开,避免看不到已录入的值
- configSectionCardStyle 由「16px 圆角 + 边框 + 内阴影 + 独立底色」改为「小标题 + 细上分割线」,
  并用 CSS 让首组不显示分割线
- modalInnerSectionStyle 同样去除边框/底色/圆角(保留内边距,间距不变)。该样式同时用于
  左侧导航容器、右侧内容面板与网络安全各分组,原先与内部分组叠成多层「卡片套卡片」
- 左侧导航项改为扁平列表:去掉 14px 圆角、边框、渐变底与投影,选中态只用左侧强调条 + 淡底
- 测试补 expandUriSection 辅助函数:两个断言 URI 文案/按钮的用例需先展开该分组。
  已确认这两项失败确由本次行为变更引起(基线 21 项全通过)
This commit is contained in:
Syngnat
2026-07-26 22:31:04 +08:00
parent c5e25d2ff5
commit daacd95193
4 changed files with 180 additions and 135 deletions

View File

@@ -414,6 +414,13 @@ body[data-theme='light'] .redis-viewer-workbench .ant-radio-button-wrapper-check
flex-shrink: 0;
}
/* 连接配置的表单分组改用「小标题 + 细分割线」代替卡片(见 ConnectionModal.tsx 的
configSectionCardStyle。每组靠一条上分割线与前一组划界因此首组不需要这条线。 */
[data-connection-config-section]:first-child {
padding-top: 0 !important;
border-top: none !important;
}
/* Custom Title Bar Close Button Hover */
body[data-platform='windows'] .titlebar-window-controls {
gap: 8px;

View File

@@ -68,6 +68,19 @@ const findClickableByAnyText = (renderer: ReactTestRenderer, texts: string[]) =>
const findClickableCard = (renderer: ReactTestRenderer, text: string) =>
renderer.root.findAll((node) => node.props?.role === "button" && textContent(node).includes(text))[0];
/**
* 展开「连接 URI」分组。
*
* 该分组默认折叠:它的多行输入框很高,展开时会把 host/账号/密码这些必填项挤出首屏,
* 因此只有编辑已含 uri 的连接时才默认展开。断言 URI 相关文案或按钮的用例需先展开。
*/
const expandUriSection = (renderer: ReactTestRenderer) => {
const toggle = renderer.root.findAll(
(node) => node.props?.["data-connection-config-section-toggle"] === "uri",
)[0];
toggle?.props?.onClick?.({ stopPropagation: () => {} });
};
const flushConnectionTestTick = async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
await Promise.resolve();
@@ -899,6 +912,10 @@ describe("ConnectionModal i18n", () => {
/>,
);
});
// URI 分组默认折叠(见 expandUriSection 的说明),先展开再断言其占位示例。
await act(async () => {
expandUriSection(renderer!);
});
expect(textContent(renderer!.toJSON())).toContain(
"For example: postgres://user:pass@127.0.0.1:5432/db_name",
);
@@ -988,6 +1005,10 @@ describe("ConnectionModal i18n", () => {
findClickableCard(renderer!, "MySQL").props.onClick();
});
await act(async () => {
expandUriSection(renderer!);
});
await act(async () => {
findButton(renderer!, "Generate URI").props.onClick();
});

View File

@@ -539,14 +539,19 @@ const ConnectionModal: React.FC<{
[overlayTheme],
);
// 弹窗内的区块容器去卡片化:原先是 14px 圆角 + 边框 + 独立底色,
// 而它同时用在左侧导航容器、右侧内容面板与网络安全的各个分组上,
// 再叠加内部分组各自的卡片,形成多层「卡片套卡片」,整页视觉噪声很高。
// 现在只保留内边距,靠内部的小标题与分割线划分层次。
// 保留原有内边距(间距不变,避免表单贴到容器边缘),只去掉边框、底色与圆角。
const modalInnerSectionStyle = useMemo(
() => ({
padding: 14,
borderRadius: 14,
border: overlayTheme.sectionBorder,
background: overlayTheme.sectionBg,
borderRadius: 0,
border: "none",
background: "transparent",
}),
[overlayTheme],
[],
);
const modalMutedTextStyle = useMemo(
@@ -759,18 +764,15 @@ const ConnectionModal: React.FC<{
</div>
);
// 表单分组改用「小标题 + 细分割线」而不是卡片:
// 原先每个分组都是 16px 圆角 + 边框 + 内阴影 + 独立底色的卡片,一屏里叠七八个,
// 视觉噪声极高且每张卡各占 16px 内边距,把真正的字段挤得很散。
// 去掉边框/圆角/底色/内阴影,只保留组间的一条上分割线来划分层次,字段密度明显提升。
const configSectionCardStyle = (): React.CSSProperties => ({
padding: 16,
borderRadius: 16,
border: darkMode
? "1px solid rgba(255,255,255,0.08)"
: "1px solid rgba(16,24,40,0.08)",
background: darkMode
? "rgba(255,255,255,0.025)"
: "rgba(255,255,255,0.70)",
boxShadow: darkMode
? "inset 0 1px 0 rgba(255,255,255,0.04)"
: "inset 0 1px 0 rgba(255,255,255,0.90)",
paddingTop: 18,
borderTop: darkMode
? "1px solid rgba(255,255,255,0.07)"
: "1px solid rgba(16,24,40,0.07)",
});
const renderConfigSectionCard = ({

View File

@@ -200,10 +200,21 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
const [readOnlyProtectionExpanded, setReadOnlyProtectionExpanded] =
React.useState(false);
// 连接 URI 属于「粘贴整串」的高级入口,默认折叠:它的多行输入框很高,
// 展开时会把 host/账号/密码这些必填项挤出首屏。
// 已经填了 URI 的连接(编辑场景)默认展开,避免用户看不到自己已录入的值。
const [uriSectionExpanded, setUriSectionExpanded] = React.useState(
() => String(initialValues?.uri || "").trim() !== "",
);
React.useEffect(() => {
setReadOnlyProtectionExpanded(false);
}, [dbType]);
React.useEffect(() => {
setUriSectionExpanded(String(initialValues?.uri || "").trim() !== "");
}, [dbType, initialValues?.uri]);
const renderStep2 = () => {
const showConnectionReadOnlyField = supportsConnectionReadOnlyMode({
type: dbType,
@@ -240,107 +251,6 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
</div>
<div style={{ display: "grid", gap: 16 }}>
{renderConfigSectionCard({
sectionKey: "identity",
icon: <ApiOutlined />,
badge: (
<Tag>
{getConnectionConfigLayoutKindLabel(connectionConfigLayout.kind)}
</Tag>
),
children: (
<>
<Form.Item
name="name"
label={t("connection.modal.field.name.label")}
>
<Input
{...noAutoCapInputProps}
placeholder={
isJVM
? t("connection.modal.field.name.placeholder.jvm")
: t("connection.modal.field.name.placeholder.default")
}
/>
</Form.Item>
<Form.Item
name="environmentType"
label={t("connection.modal.field.environment_type.label")}
initialValue={DEFAULT_CONNECTION_ENVIRONMENT}
style={{ marginBottom: 0 }}
>
<ConnectionEnvironmentSelect />
</Form.Item>
</>
),
})}
{!isCustom &&
!isJVM &&
renderConfigSectionCard({
sectionKey: "uri",
icon: <LinkOutlined />,
children: (
<>
<Form.Item
name="uri"
label={t("connection.modal.uri.label")}
help={t("connection.modal.uri.help")}
>
<Input.TextArea
{...noAutoCapInputProps}
rows={3}
placeholder={getUriPlaceholder(dbType)}
/>
</Form.Item>
{supportsConnectionParams && (
<Form.Item
name="connectionParams"
label={t("connection.modal.connectionParams.label")}
help={t("connection.modal.connectionParams.help")}
>
<Input.TextArea
{...noAutoCapInputProps}
rows={2}
placeholder={getConnectionParamsPlaceholder(dbType, oceanBaseProtocol)}
/>
</Form.Item>
)}
<Space
size={8}
style={{ marginBottom: uriFeedback ? 12 : 16 }}
wrap
>
<Button onClick={handleGenerateURI}>
{t("connection.modal.uri.action.generate")}
</Button>
<Button onClick={handleParseURI}>
{t("connection.modal.uri.action.parse")}
</Button>
<Button onClick={handleCopyURI}>
{t("connection.modal.uri.action.copy")}
</Button>
</Space>
{uriFeedback && (
<Alert
showIcon
closable
type={uriFeedback.type}
message={resolvedUriFeedbackMessage}
onClose={() => setUriFeedback(null)}
style={{ marginBottom: 16 }}
/>
)}
{renderStoredSecretControls({
fieldName: "uri",
clearKey: "opaqueURI",
hasStoredSecret: initialValues?.hasOpaqueURI,
clearLabel: t("connection.modal.uri.stored.clear"),
description: t("connection.modal.uri.stored.description"),
})}
</>
),
})}
{isCustom ? (
<>
@@ -2539,6 +2449,115 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
})}
</>
)}
{/* 必填项优先:目标地址、凭据、数据库范围由上方的条件分支渲染。
连接名称与预设类型属于可选的展示信息,连接 URI 属于「粘贴整串」的高级入口,
两者都下沉到必填项之后,避免它们(尤其是 URI 的多行输入框)把
host/账号/密码挤出首屏。 */}
{renderConfigSectionCard({
sectionKey: "identity",
icon: <ApiOutlined />,
badge: (
<Tag>
{getConnectionConfigLayoutKindLabel(connectionConfigLayout.kind)}
</Tag>
),
children: (
<>
<Form.Item
name="name"
label={t("connection.modal.field.name.label")}
>
<Input
{...noAutoCapInputProps}
placeholder={
isJVM
? t("connection.modal.field.name.placeholder.jvm")
: t("connection.modal.field.name.placeholder.default")
}
/>
</Form.Item>
<Form.Item
name="environmentType"
label={t("connection.modal.field.environment_type.label")}
initialValue={DEFAULT_CONNECTION_ENVIRONMENT}
style={{ marginBottom: 0 }}
>
<ConnectionEnvironmentSelect />
</Form.Item>
</>
),
})}
{!isCustom &&
!isJVM &&
renderConfigSectionCard({
sectionKey: "uri",
icon: <LinkOutlined />,
collapsible: true,
expanded: uriSectionExpanded,
onToggle: () => setUriSectionExpanded((expanded) => !expanded),
children: (
<>
<Form.Item
name="uri"
label={t("connection.modal.uri.label")}
help={t("connection.modal.uri.help")}
>
<Input.TextArea
{...noAutoCapInputProps}
rows={3}
placeholder={getUriPlaceholder(dbType)}
/>
</Form.Item>
{supportsConnectionParams && (
<Form.Item
name="connectionParams"
label={t("connection.modal.connectionParams.label")}
help={t("connection.modal.connectionParams.help")}
>
<Input.TextArea
{...noAutoCapInputProps}
rows={2}
placeholder={getConnectionParamsPlaceholder(dbType, oceanBaseProtocol)}
/>
</Form.Item>
)}
<Space
size={8}
style={{ marginBottom: uriFeedback ? 12 : 16 }}
wrap
>
<Button onClick={handleGenerateURI}>
{t("connection.modal.uri.action.generate")}
</Button>
<Button onClick={handleParseURI}>
{t("connection.modal.uri.action.parse")}
</Button>
<Button onClick={handleCopyURI}>
{t("connection.modal.uri.action.copy")}
</Button>
</Space>
{uriFeedback && (
<Alert
showIcon
closable
type={uriFeedback.type}
message={resolvedUriFeedbackMessage}
onClose={() => setUriFeedback(null)}
style={{ marginBottom: 16 }}
/>
)}
{renderStoredSecretControls({
fieldName: "uri",
clearKey: "opaqueURI",
hasStoredSecret: initialValues?.hasOpaqueURI,
clearLabel: t("connection.modal.uri.stored.clear"),
description: t("connection.modal.uri.stored.description"),
})}
</>
),
})}
</div>
</div>
);
@@ -3107,26 +3126,26 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
key={item.key}
type="button"
onClick={() => setActiveConfigSection(item.key)}
/* 导航项改为扁平列表:原先每项都是 14px 圆角 + 边框 + 渐变底 + 投影的
独立卡片,三项叠在一个卡片容器里形成「卡片套卡片」,视觉噪声很高。
现在只用左侧强调条 + 淡底表示选中,去掉边框、圆角与投影。 */
style={{
textAlign: "left",
padding: "12px 12px 12px 14px",
borderRadius: 14,
border: `1px solid ${
padding: "10px 10px 10px 12px",
borderRadius: 8,
border: "none",
borderLeft: `2px solid ${
active
? darkMode
? "rgba(255,214,102,0.3)"
: "rgba(24,144,255,0.24)"
: darkMode
? "rgba(255,255,255,0.045)"
: "rgba(16,24,40,0.055)"
? "#ffd666"
: "#1677ff"
: "transparent"
}`,
background: active
? darkMode
? "linear-gradient(180deg, rgba(255,214,102,0.12) 0%, rgba(255,214,102,0.06) 100%)"
: "linear-gradient(180deg, rgba(24,144,255,0.10) 0%, rgba(24,144,255,0.05) 100%)"
: darkMode
? "rgba(255,255,255,0.02)"
: "rgba(255,255,255,0.7)",
? "rgba(255,214,102,0.10)"
: "rgba(24,144,255,0.08)"
: "transparent",
color: active
? darkMode
? "#f5f7ff"
@@ -3135,12 +3154,8 @@ const ConnectionModalStep2: React.FC<ConnectionModalStep2Props> = (props) => {
? "rgba(255,255,255,0.76)"
: "#3f4b5e",
cursor: "pointer",
transition: "all 120ms ease",
boxShadow: active
? darkMode
? "0 10px 24px rgba(0,0,0,0.18)"
: "0 10px 22px rgba(24,144,255,0.08)"
: "none",
transition: "background 120ms ease, color 120ms ease",
boxShadow: "none",
}}
>
<div