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,38 +299,63 @@ 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">
<template #default="{ row }">
<span v-if="row.allowAutoRegister">自动注册 · {{ row.defaultEntitlementPlanName || "未配置权益" }}</span>
<span v-else>仅已有学员</span>
</template>
</el-table-column>
<el-table-column label="最近使用" width="180">
<template #default="{ row }">{{ formatTime(row.lastUsedAt) }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-switch v-model="row.status" :active-value="1" :inactive-value="0" @change="toggleClient(row)" />
</template>
</el-table-column>
<el-table-column label="操作" width="190" fixed="right" align="center">
<template #default="{ row }">
<div class="sso-row-actions">
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
<el-button size="small" @click="rotateSecret(row)">更新密钥</el-button>
</div>
</template>
</el-table-column>
</el-table>
<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="回跳地址" min-width="170" show-overflow-tooltip>
<template #default="{ row }">{{ row.redirectUris.join("、") || "未配置" }}</template>
</el-table-column>
<el-table-column label="运行状态" width="148">
<template #default="{ 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="150" align="right">
<template #default="{ row }">
<div class="sso-row-actions">
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
<el-button size="small" @click="rotateSecret(row)">更新密钥</el-button>
</div>
</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">
<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>
</div>
<el-form-item label="允许回跳地址">
<el-input v-model="clientForm.redirectUrisText" type="textarea" :rows="4" placeholder="每行一个完整 HTTPS 地址;不需要回跳时可以留空" />
</el-form-item>
<div class="sso-auto-register">
<div>
<strong>自动注册新学员</strong>
<p>可信应用传入本地尚不存在的已验证手机号和姓名时自动创建学员并分配默认权益</p>
<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>
<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%">
<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 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>
</div>
</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>
</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>
<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>
</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>