feat(auth): 注册服务协议 - 名称与条款改为后台可配置并同步注册页展示

后端:
- RegistrationSettingRequest 协议字段改为可选指针,注册开关单独保存不再覆盖协议正文
- 新增 RegistrationAgreement / AgreementTitleForMessage:标题未配置时跟随品牌名生成,读取失败返回空标题而非猜一个不存在的协议名
- PublicAuthSettings 下发 agreementTitle / agreementContent,首用户分支同样携带
- 新增 registration_agreement_test.go 覆盖品牌名兜底、存储合并与失败降级

前端:
- 注册页协议标题与条款改为消费后台配置;读取失败时隐藏协议入口并支持重新读取
- 验证码获取按钮内联到输入框右侧,联系方式为空即禁用;sms_email 双通道合并为单输入框
- 协议弹窗重排:条目标题独立成行加重、空态虚线提示、底部补充同意说明;深浅主题适配
- 协议勾选行改为状态化区域,复选框适配 AntD 6 结构(方框在 .ant-checkbox 本体,修正深色主题白底白勾)
- 后台「服务协议名称与条款」区块补齐表单内边距并对齐区块头/页脚,协议名称限宽

文档:http-api.mdx 更新协议字段语义与失败降级说明;pending-test.mdx 增加手工验证清单
This commit is contained in:
✨await committed 2026-09-24 16:56:44 +08:00
1 parent 2b774a2ec7
commit af490ee610
16 files changed
+977 -74

No files matched your search

+78 -2
View File
@@ -3275,6 +3275,80 @@
line-height: 1.5;
}
/* --- 服务协议条款编辑区 -------------------------------------------------------
模板入口与字数提示紧贴正文编辑框:它们是「填写条款」这一步的辅助操作,
放回表单内部后与保存行(页脚)在视觉上分离,避免被读成两个并列的主操作。
内边距:同页其他区块(注册策略、Linux.do)的表单都包在带边框和内边距的
子卡片里,本区块的 Form.Item 是裸的,不加 padding 会左右贴死卡片边缘,
与区块头(20px)和页脚(14px 20px)对不齐。窄屏 16px,≥1024px 与
summary 的 20px 对齐。 */
.admin-access-agreement-section > .admin-settings-section-content > .ant-form {
padding: 14px 16px 8px;
}
@media (min-width: 1024px) {
.admin-access-agreement-section > .admin-settings-section-content > .ant-form {
padding: 18px 20px 10px;
}
}
/* 协议名称是短文本,占满 1400px 宽的卡片视觉重心失衡;限宽到表单常用档位。 */
.admin-access-agreement-title-field {
max-width: 480px;
}
.admin-access-agreement-content-field.ant-form-item {
margin-bottom: 10px;
}
.admin-access-agreement-toolbar {
display: flex;
min-width: 0;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 14px;
margin-bottom: 4px;
border: 1px solid var(--admin-card-border);
border-radius: 8px;
background: color-mix(in srgb, var(--admin-layer-2) 48%, var(--admin-layer-1));
padding: 8px 10px;
}
.admin-access-agreement-template-note {
display: flex;
min-width: 0;
align-items: center;
gap: 7px;
color: var(--admin-text-muted);
font-size: 11px;
line-height: 1.5;
}
.admin-access-agreement-template-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
}
.admin-access-agreement-metric {
color: var(--admin-text-muted);
font-size: 11px;
font-variant-numeric: tabular-nums;
line-height: 1.5;
white-space: nowrap;
}
/* 内容为空时字数提示没有信息量,窄屏优先让位给操作按钮。 */
@media (max-width: 640px) {
.admin-access-agreement-toolbar {
align-items: flex-start;
flex-direction: column;
}
}
.admin-access-load-error,
.admin-access-loading-card {
min-height: 220px;
@@ -6230,6 +6304,7 @@
.admin-email-delivery-section,
.admin-access-registration-section,
.admin-access-linuxdo-section,
.admin-access-agreement-section,
.admin-intercept-policy-section,
.admin-third-party-credential-section {
border-color: color-mix(in srgb, var(--foreground) 22%, var(--admin-card-border)) !important;
@@ -6239,17 +6314,18 @@
.admin-email-delivery-section > .admin-settings-section-summary,
.admin-access-registration-section > .admin-settings-section-summary,
.admin-access-linuxdo-section > .admin-settings-section-summary,
.admin-access-agreement-section > .admin-settings-section-summary,
.admin-intercept-policy-section > .admin-settings-section-summary,
.admin-third-party-credential-section > .admin-settings-section-summary {
background: color-mix(in srgb, var(--foreground) 3%, var(--admin-layer-1));
}
:is(.admin-storage-mode-section, .admin-email-delivery-section, .admin-access-registration-section, .admin-access-linuxdo-section, .admin-intercept-policy-section, .admin-third-party-credential-section) > .admin-settings-section-summary h2 {
:is(.admin-storage-mode-section, .admin-email-delivery-section, .admin-access-registration-section, .admin-access-linuxdo-section, .admin-access-agreement-section, .admin-intercept-policy-section, .admin-third-party-credential-section) > .admin-settings-section-summary h2 {
font-size: 16px;
letter-spacing: -0.01em;
}
:is(.admin-storage-mode-section, .admin-email-delivery-section, .admin-access-registration-section, .admin-access-linuxdo-section, .admin-intercept-policy-section, .admin-third-party-credential-section) > .admin-settings-section-summary p {
:is(.admin-storage-mode-section, .admin-email-delivery-section, .admin-access-registration-section, .admin-access-linuxdo-section, .admin-access-agreement-section, .admin-intercept-policy-section, .admin-third-party-credential-section) > .admin-settings-section-summary p {
max-width: 760px;
margin-top: 5px;
font-size: 12px;
+283
View File
@@ -301,6 +301,10 @@
--auth-card-bg: #ffffff;
--auth-accent: #2563eb;
--auth-muted: #737373;
/* 协议勾选框的填充色与对勾色。必须成对维护:两者相近会在某一主题下
退化成看不见的勾选态。深色主题下这里与 --foreground 的白色相反。 */
--auth-check-bg: #171717;
--auth-check-fg: #ffffff;
/* 可配置皮肤的组件入口。经典主题保持这些项目原始值;自定义主题在
React 启动前由公开外观配置覆盖,页面和组件只消费这些语义变量。 */
@@ -474,6 +478,162 @@
color: var(--foreground);
}
/* --- 注册页协议同意行 ---------------------------------------------------------
勾选热区必须覆盖“文字 + 协议名”整段,但协议名同时是可点击的阅读入口,
因此热区由 label 承担,协议名按钮用 stopPropagation 之外的方式独立可点:
这里只做视觉与排布,行为仍由 React 侧控制。 */
.auth-agreement-row {
display: flex;
align-items: flex-start;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border: var(--control-border-width) solid transparent;
border-radius: var(--r-sm);
color: var(--auth-muted);
font-size: var(--fs-label);
line-height: 1.65;
transition:
border-color var(--motion-instant) ease-out,
background-color var(--motion-instant) ease-out,
color var(--motion-instant) ease-out;
}
.auth-agreement-row:hover {
background: color-mix(in srgb, var(--foreground) 4%, transparent);
}
/* 已勾选时用极轻的强调底色标记“这一步已完成”,不做成第二主色。 */
.auth-agreement-row[data-accepted="true"] {
border-color: color-mix(in srgb, var(--foreground) 10%, transparent);
background: color-mix(in srgb, var(--foreground) 4.5%, transparent);
color: color-mix(in srgb, var(--foreground) 78%, transparent);
}
/* 未勾选时给协议名一个克制的提示底色,指向“这里还没有同意”。 */
.auth-agreement-row[data-accepted="false"] .auth-agreement-link {
text-decoration-line: underline;
text-decoration-color: color-mix(in srgb, var(--auth-accent) 42%, transparent);
text-decoration-thickness: 1px;
text-underline-offset: 3px;
}
.auth-agreement-row .ant-checkbox-wrapper {
flex: 0 0 auto;
margin-inline-end: 0;
color: inherit;
font-size: inherit;
line-height: inherit;
}
/* AntD 默认把 checkbox 行高 22px 与文案首行错开 1px 左右,这里统一到首行。 */
.auth-agreement-row .ant-checkbox {
align-self: flex-start;
top: 0.1em;
}
.auth-agreement-row .ant-checkbox + span {
padding-inline-start: var(--space-2);
padding-inline-end: 0;
}
.auth-agreement-label {
color: inherit;
}
.auth-agreement-link {
display: inline;
margin: 0;
padding: 0;
border: 0;
background: none;
color: var(--auth-accent);
font: inherit;
line-height: inherit;
text-align: start;
transition: color var(--motion-instant) ease-out;
}
.auth-agreement-link:hover {
color: var(--foreground);
}
.auth-agreement-link:focus-visible {
border-radius: 3px;
outline: var(--focus-ring-width) solid var(--ring);
outline-offset: 3px;
}
/* --- 协议阅读弹窗 -------------------------------------------------------------
长条款需要稳定的阅读列宽和可预期的滚动边界;正文段落之间用间距而不是
空行撑开,避免后台多段文本被渲染成松散的空洞。 */
.auth-agreement-modal .ant-modal-container {
border-radius: var(--r-lg);
}
.auth-agreement-modal .ant-modal-header {
margin-bottom: var(--space-1);
}
.auth-agreement-modal .ant-modal-title {
font-size: var(--fs-title-sm, 16px);
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.5;
}
.auth-agreement-body {
max-height: min(56vh, 460px);
overflow-y: auto;
padding-inline-end: var(--space-3);
overscroll-behavior: contain;
color: color-mix(in srgb, var(--foreground) 74%, transparent);
font-size: var(--fs-body, 13px);
line-height: 1.85;
}
.auth-agreement-body p {
margin: 0;
}
.auth-agreement-body p + p {
margin-top: var(--space-4);
}
/* 启用“一、二、三”式条目标题时,标题段单独加重,正文段保持常规字重。 */
.auth-agreement-body p[data-role="heading"] {
margin-top: var(--space-5);
color: var(--foreground);
font-weight: 600;
}
.auth-agreement-body p[data-role="heading"]:first-child {
margin-top: 0;
}
.auth-agreement-empty {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-4) var(--space-3);
border: var(--control-border-width) dashed color-mix(in srgb, var(--foreground) 16%, transparent);
border-radius: var(--r-md);
color: var(--auth-muted);
font-size: var(--fs-label);
justify-content: center;
}
.auth-agreement-meta {
display: flex;
align-items: center;
gap: var(--space-2);
margin-top: var(--space-4);
padding-top: var(--space-3);
border-top: var(--control-border-width) solid var(--border);
color: var(--auth-muted);
font-size: var(--fs-micro, 11px);
letter-spacing: 0.01em;
}
.site-compliance-footer {
display: flex;
min-width: 0;
@@ -799,6 +959,10 @@
--auth-card-bg: #121318;
--auth-accent: #93c5fd;
--auth-muted: #8a8b91;
/* 深色主题下勾选框用浅底 + 深勾,与浅色主题的深底 + 白勾形成互补;
直接复用 --foreground 会得到白底白勾。 */
--auth-check-bg: #e5e5e5;
--auth-check-fg: #171717;
--foreground: #ffffff;
--border: #222222;
--ring: oklch(0.556 0 0);
@@ -6997,6 +7161,125 @@ html:not(.dark) .creation-studio-control[aria-expanded="true"] { background: #e7
caret-color: var(--foreground);
}
/* 验证码输入行:输入框与获取按钮同排,按钮贴右侧并用竖线分隔。
倒计时/禁用时按钮降到弱化态,避免和主提交按钮争夺注意力。 */
.auth-code-field {
display: flex;
align-items: stretch;
gap: 0;
border: var(--control-border-width) solid transparent;
border-radius: var(--radius, var(--r-sm));
background: color-mix(in srgb, var(--foreground) 5.5%, transparent);
transition:
border-color var(--motion-instant) ease-out,
background-color var(--motion-instant) ease-out,
box-shadow var(--motion-instant) ease-out;
}
.auth-code-field:hover {
border-color: var(--border);
background: color-mix(in srgb, var(--foreground) 7%, transparent);
}
/* 焦点环挂在整行容器上,内部输入框不再叠加第二层高亮。 */
.auth-code-field:focus-within {
border-color: var(--ring);
background: color-mix(in srgb, var(--foreground) 8.5%, transparent);
box-shadow: 0 0 0 var(--focus-ring-width) color-mix(in srgb, var(--ring) 18%, transparent);
}
.auth-code-field .ant-input,
.auth-code-field .ant-input-affix-wrapper {
border: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
.auth-code-field .ant-input:focus,
.auth-code-field .ant-input-affix-wrapper-focused {
border-color: transparent !important;
box-shadow: none !important;
}
.auth-code-send {
flex: 0 0 auto;
align-self: center;
height: auto;
min-height: 26px;
margin-inline-end: 5px;
padding-inline: var(--space-3);
border: 0;
border-inline-start: var(--control-border-width) solid color-mix(in srgb, var(--foreground) 12%, transparent);
border-radius: 0;
background: transparent;
box-shadow: none;
color: var(--auth-accent);
font-size: var(--fs-label);
font-weight: 500;
white-space: nowrap;
}
.auth-code-send:hover:not(:disabled) {
background: transparent;
color: var(--foreground);
}
/* 倒计时/发送中:保持排版稳定,只做颜色降级,不改变宽度导致输入框跳动。 */
.auth-code-send:disabled,
.auth-code-send[data-counting="true"] {
border-inline-start-color: color-mix(in srgb, var(--foreground) 10%, transparent);
background: transparent !important;
color: var(--auth-muted);
font-variant-numeric: tabular-nums;
}
.auth-code-field.is-busy .ant-input {
color: var(--auth-muted);
}
/* 协议勾选框:圆角方框 + 主题主色填充。
注意 AntD 6 把方框样式渲染在 .ant-checkbox 本身(对齐 antd/es/checkbox/style),
.ant-checkbox-inner 在内联样式阶段已被移除,因此所有规则必须打在外层元素上,
否则样式不会生效。
另外方框尺寸来自 checkboxSize = controlHeight / 2(本主题下为 18px),
由 cssinjs 生成,选择器权重不低于这里的规则,所以尺寸必须用 !important 收敛到 16px,
否则视觉上会是一个偏大的方块。对勾是 .ant-checkbox::after 的边框,
默认取 colorWhite,深色主题下 colorPrimary 也是近白,会退化成白底白勾,需单独覆盖。 */
.auth-agreement-row .ant-checkbox {
align-self: center;
width: 16px !important;
height: 16px !important;
border-color: color-mix(in srgb, var(--foreground) 32%, transparent) !important;
border-radius: var(--checkbox-radius, 4px);
background: color-mix(in srgb, var(--foreground) 3%, transparent) !important;
transition:
border-color var(--motion-instant) ease-out,
background-color var(--motion-instant) ease-out;
}
.auth-agreement-row .ant-checkbox::after {
border-color: var(--auth-check-fg) !important;
border-width: 2px !important;
}
.auth-agreement-row .ant-checkbox-wrapper:hover .ant-checkbox:not(.ant-checkbox-checked),
.auth-agreement-row .ant-checkbox:hover:not(.ant-checkbox-checked) {
border-color: color-mix(in srgb, var(--foreground) 55%, transparent) !important;
background: color-mix(in srgb, var(--foreground) 6%, transparent) !important;
}
.auth-agreement-row .ant-checkbox-checked,
.auth-agreement-row .ant-checkbox-wrapper:hover .ant-checkbox-checked,
.auth-agreement-row .ant-checkbox-checked:hover {
border-color: var(--auth-check-bg) !important;
background: var(--auth-check-bg) !important;
}
.auth-agreement-row .ant-checkbox:has(.ant-checkbox-input:focus-visible) {
outline: none;
box-shadow: 0 0 0 var(--focus-ring-width) color-mix(in srgb, var(--ring) 20%, transparent);
}
:where(.ant-modal, .ant-drawer, .ant-select-dropdown, .ant-dropdown, .ant-popover) {
overscroll-behavior: contain;
}