fix: optimize sso integration layout

This commit is contained in:
2026-08-03 19:11:41 +08:00
parent cd3c875106
commit c4de4e8c56
4 changed files with 521 additions and 68 deletions

View File

@@ -299,30 +299,39 @@ function showError(error: unknown, fallback: string) {
<el-button :loading="savingUrl" @click="savePublicUrl">保存地址</el-button>
</section>
<el-tabs :model-value="activeTab" @tab-change="switchTab">
<el-tabs class="sso-tabs" :model-value="activeTab" @tab-change="switchTab">
<el-tab-pane label="接入应用" name="applications">
<el-table :data="clients" stripe empty-text="还没有接入应用">
<el-table-column prop="name" label="应用名称" min-width="180" />
<el-table-column prop="appId" label="应用ID" min-width="180" />
<el-table-column label="回跳地址" min-width="240" show-overflow-tooltip>
<template #default="{ row }">{{ row.redirectUris.join("、") || "未配置" }}</template>
</el-table-column>
<el-table-column prop="identityCount" label="绑定用户" width="100" align="center" />
<el-table-column label="新用户策略" min-width="190">
<div class="sso-table-shell">
<el-table :data="clients" stripe empty-text="还没有接入应用" table-layout="fixed" style="width: 100%">
<el-table-column label="应用" min-width="170">
<template #default="{ row }">
<div class="sso-app-cell">
<strong>{{ row.name }}</strong>
<span>{{ row.appId }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="账号策略" min-width="170">
<template #default="{ row }">
<div class="sso-policy-cell">
<span v-if="row.allowAutoRegister">自动注册 · {{ row.defaultEntitlementPlanName || "未配置权益" }}</span>
<span v-else>仅已有学员</span>
<small>已绑定 {{ row.identityCount }} </small>
</div>
</template>
</el-table-column>
<el-table-column label="最近使用" width="180">
<template #default="{ row }">{{ formatTime(row.lastUsedAt) }}</template>
<el-table-column label="回跳地址" min-width="170" show-overflow-tooltip>
<template #default="{ row }">{{ row.redirectUris.join("、") || "未配置" }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<el-table-column label="运行状态" width="148">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="toggleClient(row)" />
<div class="sso-status-cell">
<div><el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="toggleClient(row)" /><span>{{ row.status === 1 ? "已启用" : "已停用" }}</span></div>
<small>最近使用{{ formatTime(row.lastUsedAt) }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="190" fixed="right" align="center">
<el-table-column label="操作" width="150" align="right">
<template #default="{ row }">
<div class="sso-row-actions">
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
@@ -331,6 +340,22 @@ function showError(error: unknown, fallback: string) {
</template>
</el-table-column>
</el-table>
</div>
<div class="sso-app-cards">
<el-empty v-if="!clients.length" description="还没有接入应用" :image-size="72" />
<article v-for="client in clients" v-else :key="client.id" class="sso-app-card">
<div class="sso-app-card-head">
<div class="sso-app-cell"><strong>{{ client.name }}</strong><span>{{ client.appId }}</span></div>
<div class="sso-card-status"><el-switch v-model="client.status" :active-value="1" :inactive-value="0" @change="toggleClient(client)" /><span>{{ client.status === 1 ? "已启用" : "已停用" }}</span></div>
</div>
<dl>
<div><dt>账号策略</dt><dd>{{ client.allowAutoRegister ? `自动注册 · ${client.defaultEntitlementPlanName || "未配置权益"}` : "仅已有学员" }}已绑定 {{ client.identityCount }} </dd></div>
<div><dt>回跳地址</dt><dd>{{ client.redirectUris.join("、") || "未配置" }}</dd></div>
<div><dt>最近使用</dt><dd>{{ formatTime(client.lastUsedAt) }}</dd></div>
</dl>
<div class="sso-card-actions"><el-button size="small" @click="openEditDialog(client)">编辑</el-button><el-button size="small" @click="rotateSecret(client)">更新密钥</el-button></div>
</article>
</div>
</el-tab-pane>
<el-tab-pane label="账号绑定" name="identities">
@@ -386,28 +411,50 @@ function showError(error: unknown, fallback: string) {
</el-tab-pane>
</el-tabs>
<el-dialog v-model="clientDialogOpen" :title="dialogTitle" width="620px" destroy-on-close>
<el-form label-position="top">
<el-dialog v-model="clientDialogOpen" class="sso-client-dialog" :title="dialogTitle" width="min(720px, calc(100vw - 32px))" destroy-on-close align-center>
<el-form class="sso-client-form" label-position="top">
<section class="sso-form-section">
<div class="sso-section-heading">
<strong>基本信息</strong>
<span>用于识别接入方应用 ID 创建后不可修改</span>
</div>
<div class="sso-form-grid">
<el-form-item label="应用名称"><el-input v-model="clientForm.name" placeholder="例如:学员服务 App" /></el-form-item>
<el-form-item label="应用ID"><el-input v-model="clientForm.appId" :disabled="Boolean(editingClientId)" placeholder="例如student-app" /></el-form-item>
<el-form-item label="应用 ID"><el-input v-model="clientForm.appId" :disabled="Boolean(editingClientId)" placeholder="例如student-app" /></el-form-item>
</div>
<el-form-item label="允许回跳地址">
<el-input v-model="clientForm.redirectUrisText" type="textarea" :rows="4" placeholder="每行一个完整 HTTPS 地址;不需要回跳时可以留空" />
</section>
<section class="sso-form-section">
<div class="sso-section-heading">
<strong>登录后回跳</strong>
<span>每行一个完整 HTTPS 地址不需要回跳时可以留空</span>
</div>
<el-form-item class="sso-redirect-field" label="允许回跳地址">
<el-input v-model="clientForm.redirectUrisText" type="textarea" :rows="3" placeholder="https://student.example.com/home" />
</el-form-item>
<div class="sso-auto-register">
<div>
<strong>自动注册新学员</strong>
<p>可信应用传入本地尚不存在的已验证手机号和姓名时自动创建学员并分配默认权益</p>
</section>
<section class="sso-form-section">
<div class="sso-section-heading">
<strong>访问与账号策略</strong>
<span>控制应用是否可用以及首次进入时如何处理新学员</span>
</div>
<div class="sso-setting-list">
<div class="sso-setting-row">
<div><strong>启用应用</strong><span>停用后该应用将无法继续申请或使用免登录</span></div>
<el-switch v-model="clientForm.status" :active-value="1" :inactive-value="0" />
</div>
<div class="sso-setting-row">
<div><strong>自动注册新学员</strong><span>传入已验证手机号和姓名时可自动创建账号并分配权益</span></div>
<el-switch v-model="clientForm.allowAutoRegister" />
</div>
<el-form-item v-if="clientForm.allowAutoRegister" label="默认权益版本">
<el-select v-model="clientForm.defaultEntitlementPlanId" placeholder="请选择自动注册学员的权益" style="width: 100%">
<div v-if="clientForm.allowAutoRegister" class="sso-entitlement-setting">
<el-form-item label="新学员默认权益">
<el-select v-model="clientForm.defaultEntitlementPlanId" placeholder="请选择权益版本" style="width: 100%">
<el-option v-for="plan in entitlementPlans" :key="plan.id" :label="plan.name" :value="plan.id" />
</el-select>
</el-form-item>
<el-form-item label="启用应用"><el-switch v-model="clientForm.status" :active-value="1" :inactive-value="0" /></el-form-item>
</div>
</div>
</section>
</el-form>
<template #footer>
<el-button @click="clientDialogOpen = false">取消</el-button>
@@ -430,21 +477,382 @@ function showError(error: unknown, fallback: string) {
</template>
<style scoped>
.sso-page { display: grid; gap: 20px; }
.sso-public-url { display: grid; grid-template-columns: minmax(250px, 1fr) minmax(320px, 1.3fr) auto; gap: 18px; align-items: center; padding: 20px 22px; border: 1px solid #dce8e4; border-radius: 16px; background: #fff; }
.sso-public-url p { margin: 6px 0 0; color: #71817d; font-size: 13px; }
.sso-row-actions { display: flex; justify-content: center; gap: 8px; white-space: nowrap; }
.sso-filters { display: flex; gap: 12px; margin-bottom: 16px; }
.sso-filters .el-select { width: 190px; }
.sso-filters .el-input { max-width: 360px; }
.sso-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.sso-auto-register { display: flex; align-items: center; justify-content: space-between; gap: 24px; margin-bottom: 18px; padding: 16px; border: 1px solid #dce8e4; border-radius: 12px; background: #f8fbfa; }
.sso-auto-register p { margin: 6px 0 0; color: #71817d; font-size: 13px; line-height: 1.6; }
.sso-secret-block { display: grid; grid-template-columns: 90px minmax(0, 1fr); gap: 12px 16px; align-items: start; margin-top: 20px; }
.sso-secret-block label { color: #6c7d79; }
.sso-secret-block code { padding: 10px 12px; overflow-wrap: anywhere; border-radius: 8px; background: #f3f7f5; color: #173d34; }
@media (max-width: 980px) {
.sso-public-url { grid-template-columns: 1fr; }
.sso-filters { flex-wrap: wrap; }
.sso-page {
container-type: inline-size;
display: grid;
gap: 20px;
width: 100%;
min-width: 0;
max-width: 100%;
overflow: hidden;
}
.sso-public-url {
display: grid;
grid-template-columns: minmax(230px, 1fr) minmax(280px, 1.2fr) auto;
gap: 18px;
align-items: center;
min-width: 0;
padding: 20px 22px;
border: 1px solid #dce8e4;
border-radius: 16px;
background: #fff;
}
.sso-public-url > *,
.sso-tabs,
.sso-table-shell {
min-width: 0;
}
.sso-public-url p {
margin: 6px 0 0;
color: #71817d;
font-size: 13px;
}
:deep(.sso-tabs .el-tabs__content),
:deep(.sso-tabs .el-tab-pane) {
width: 100%;
min-width: 0;
max-width: 100%;
}
.sso-table-shell {
width: 100%;
max-width: 100%;
overflow: hidden;
border: 1px solid #e1eae7;
border-radius: 14px;
background: #fff;
}
.sso-app-cell,
.sso-policy-cell,
.sso-status-cell {
display: grid;
gap: 5px;
min-width: 0;
}
.sso-app-cell strong,
.sso-policy-cell span {
overflow: hidden;
color: #253b35;
text-overflow: ellipsis;
white-space: nowrap;
}
.sso-app-cell span,
.sso-policy-cell small,
.sso-status-cell small {
overflow: hidden;
color: #80908b;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.sso-status-cell > div,
.sso-card-status {
display: flex;
gap: 8px;
align-items: center;
color: #4f625c;
font-size: 13px;
}
.sso-row-actions,
.sso-card-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
white-space: nowrap;
}
.sso-row-actions :deep(.el-button + .el-button),
.sso-card-actions :deep(.el-button + .el-button) {
margin-left: 0;
}
.sso-app-cards {
display: none;
}
.sso-filters {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.sso-filters .el-select {
width: 190px;
}
.sso-filters .el-input {
max-width: 360px;
}
.sso-client-form {
display: grid;
gap: 16px;
}
.sso-form-section {
min-width: 0;
padding: 18px 20px;
border: 1px solid #e0e9e6;
border-radius: 14px;
background: #fbfcfc;
}
.sso-section-heading {
display: grid;
gap: 4px;
margin-bottom: 16px;
}
.sso-section-heading strong {
color: #203a33;
font-size: 15px;
}
.sso-section-heading span {
color: #7a8b86;
font-size: 13px;
line-height: 1.55;
}
.sso-form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.sso-client-form :deep(.el-form-item) {
min-width: 0;
margin-bottom: 0;
}
.sso-client-form :deep(.el-form-item__label) {
height: auto;
padding-bottom: 7px;
color: #465a54;
font-weight: 600;
line-height: 1.4;
}
.sso-client-form :deep(.el-input__wrapper) {
min-height: 42px;
}
.sso-redirect-field :deep(.el-textarea__inner) {
min-height: 82px !important;
padding: 11px 13px;
line-height: 1.55;
}
.sso-setting-list {
overflow: hidden;
border: 1px solid #e0e9e6;
border-radius: 12px;
background: #fff;
}
.sso-setting-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 24px;
align-items: center;
min-height: 72px;
padding: 14px 16px;
}
.sso-setting-row + .sso-setting-row,
.sso-entitlement-setting {
border-top: 1px solid #e7eeec;
}
.sso-setting-row > div {
display: grid;
gap: 5px;
min-width: 0;
}
.sso-setting-row strong {
color: #324942;
font-size: 14px;
}
.sso-setting-row span {
color: #7a8b86;
font-size: 13px;
line-height: 1.5;
}
.sso-entitlement-setting {
padding: 14px 16px 16px;
background: #f5faf8;
}
:deep(.sso-client-dialog) {
max-height: calc(100vh - 32px);
overflow: hidden;
border-radius: 18px;
}
:deep(.sso-client-dialog .el-dialog__header) {
margin-right: 0;
padding: 22px 26px 17px;
border-bottom: 1px solid #e7eeec;
}
:deep(.sso-client-dialog .el-dialog__title) {
color: #1f3932;
font-size: 21px;
font-weight: 700;
}
:deep(.sso-client-dialog .el-dialog__body) {
max-height: calc(100vh - 178px);
overflow-y: auto;
padding: 20px 26px;
}
:deep(.sso-client-dialog .el-dialog__footer) {
padding: 16px 26px 20px;
border-top: 1px solid #e7eeec;
}
.sso-secret-block {
display: grid;
grid-template-columns: 90px minmax(0, 1fr);
gap: 12px 16px;
align-items: start;
margin-top: 20px;
}
.sso-secret-block label {
color: #6c7d79;
}
.sso-secret-block code {
padding: 10px 12px;
overflow-wrap: anywhere;
border-radius: 8px;
background: #f3f7f5;
color: #173d34;
}
@container (max-width: 940px) {
.sso-public-url {
grid-template-columns: minmax(0, 1fr) auto;
}
.sso-public-url > div {
grid-column: 1 / -1;
}
.sso-table-shell {
display: none;
}
.sso-app-cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.sso-app-cards > .el-empty {
grid-column: 1 / -1;
}
.sso-app-card {
display: grid;
gap: 16px;
min-width: 0;
padding: 18px;
border: 1px solid #dfe9e6;
border-radius: 14px;
background: #fff;
}
.sso-app-card-head {
display: flex;
gap: 16px;
align-items: flex-start;
justify-content: space-between;
}
.sso-card-status {
flex: 0 0 auto;
}
.sso-app-card dl {
display: grid;
gap: 12px;
margin: 0;
}
.sso-app-card dl > div {
display: grid;
gap: 4px;
min-width: 0;
}
.sso-app-card dt {
color: #82918d;
font-size: 12px;
}
.sso-app-card dd {
margin: 0;
overflow-wrap: anywhere;
color: #41554f;
font-size: 13px;
line-height: 1.55;
}
}
@container (max-width: 660px) {
.sso-app-cards,
.sso-public-url {
grid-template-columns: minmax(0, 1fr);
}
.sso-public-url > div {
grid-column: auto;
}
.sso-public-url .el-button {
width: 100%;
}
.sso-filters {
flex-wrap: wrap;
}
.sso-filters .el-select,
.sso-filters .el-input {
width: 100%;
max-width: none;
}
}
@media (max-width: 760px) {
.sso-form-grid {
grid-template-columns: 1fr;
}
.sso-form-section {
padding: 16px;
}
:deep(.sso-client-dialog .el-dialog__header),
:deep(.sso-client-dialog .el-dialog__body),
:deep(.sso-client-dialog .el-dialog__footer) {
padding-right: 18px;
padding-left: 18px;
}
}
</style>

View File

@@ -162,3 +162,48 @@ final result: passed
- 未发现剩余 P0、P1、P2 问题。
final result: passed
---
# 应用接入页面设计验收
- 源视觉依据:`/var/folders/hn/vg5lxj0j60d2cp0k6vv_k0mw0000gn/T/codex-clipboard-70a7e9c2-c058-4665-b236-e73ccef6d2b0.png`
- 浏览器实现截图:`development_records/ui-evidence/sso-integration-2026-08-03/implementation.png`
- 同屏对比图:`development_records/ui-evidence/sso-integration-2026-08-03/comparison.png`
- 验收地址:`http://localhost:5174/`
- 状态:管理员已登录,进入“应用接入”,打开“新增接入应用”弹窗
- 视口1380 × 900 CSS px设备像素密度 1
- 源图像素1380 × 1026实现图像素1380 × 900
- 归一化方式:保持两张图 1380 px 等宽,不缩放;对比图仅将实现图底部补白至 1026 px避免密度或缩放造成误判
## 全视图对比证据
- 原界面将基础字段、回跳地址、自动注册和启用开关连续堆叠,字段说明与控件边界不清,底部留白失衡。
- 新界面拆分为“基本信息”“登录后回跳”“访问与账号策略”三个语义区,主次关系、上下间距与操作路径清晰。
- 应用列表合并重复维度并限制列宽;在容器宽度不足时切换为卡片布局,不再依赖横向滚动。
## 聚焦区域对比证据
- 表单区:输入框高度、标签间距、两列表单对齐、文本域高度均保持一致;说明文字从占位符中分离,阅读层级更稳定。
- 策略区:“启用应用”和“自动注册新学员”采用同一行式设置组件,开关右对齐;自动注册开启后才展示默认权益,减少无关内容。
- 标签页:在 1380 px 视口下,页面、标签内容和表格的 `scrollWidth` 均等于 `clientWidth`;无页面级横向滚动。
## 发现项
- 未发现仍需处理的 P0、P1 或 P2 问题。
- P3后台整体框架在小于约 1180 px 的窗口仍有全局最小宽度限制;该行为不是本次应用接入页面引入,且不影响当前桌面端验收范围。
## 交互与运行验证
- 已验证进入应用接入、切换“接入应用 / 账号绑定 / 登录审计”、打开与关闭新增应用弹窗;自动注册开关可正常显示和隐藏默认权益配置。
- 已验证 1380 × 900 与 1280 × 720 桌面视口;弹窗内容过高时仅弹窗正文区域滚动,头部和底部操作保持可见。
- 已检查浏览器控制台,新增错误和警告为 0。
- 已执行前端类型检查与生产构建,构建通过。
## 对比历史
1. 初始问题:弹窗内容拥挤、策略控件分散;应用列表固定列宽总和过大,引发横向滚动。
2. 修复:分区重构弹窗;合并列表信息列;补充容器宽度约束和窄容器卡片布局。
3. 复验:同屏对比中信息层级与留白已改善;桌面视口下页面级横向滚动已消除。
final result: passed

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB