chore: 优化passkey

This commit is contained in:
xiaojunnuo
2026-03-15 02:20:39 +08:00
parent bbef854c02
commit f642e42eea
8 changed files with 204 additions and 223 deletions
@@ -45,79 +45,86 @@
</div>
</div>
<div v-if="settingStore.sysPublic.oauthEnabled && settingStore.isPlus" class="bindings-card md:rounded">
<div class="card-title">
<fs-icon icon="ion:link-outline" class="title-icon" />
<span>第三方账号绑定</span>
</div>
<div class="bindings-list">
<template v-for="item in computedOauthBounds" :key="item.name">
<div v-if="item.addonId" class="binding-item">
<div class="binding-icon">
<fs-icon :icon="item.icon" class="icon" />
</div>
<div class="binding-info">
<span class="binding-name">{{ item.title }}</span>
<span class="binding-status" :class="item.bound ? 'bound' : 'unbound'">
{{ item.bound ? "已绑定" : "未绑定" }}
</span>
</div>
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item.name)">
<template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑
</a-button>
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)">
<template #icon><fs-icon icon="ion:link-outline" /></template>
绑定
</a-button>
<div class="flex flex-wrap">
<div class="w-full md:w-1/2 md:pr-2">
<div v-if="settingStore.sysPublic.oauthEnabled && settingStore.isPlus" class="bindings-card md:rounded">
<div class="card-title">
<fs-icon icon="ion:link-outline" class="title-icon" />
<span>第三方账号绑定</span>
</div>
</template>
<div v-if="computedOauthBounds.length === 0" class="empty-text">暂无可用的第三方账号绑定</div>
</div>
</div>
<div v-if="settingStore.sysPublic.passkeyEnabled && settingStore.isPlus" class="passkey-card md:rounded">
<div class="card-title">
<fs-icon icon="ion:finger-print" class="title-icon" />
<span>Passkey 安全密钥</span>
</div>
<div class="passkey-list">
<div v-for="passkey in passkeys" :key="passkey.id" class="passkey-item">
<div class="passkey-icon">
<fs-icon icon="ion:finger-print" class="icon" />
<div class="bindings-list">
<template v-for="item in computedOauthBounds" :key="item.name">
<div v-if="item.addonId" class="binding-item">
<div class="binding-icon">
<fs-icon :icon="item.icon" class="icon" />
</div>
<div class="binding-info">
<span class="binding-name">{{ item.title }}</span>
<span>
<a-tag v-if="item.bound" color="green" class="bound-tag1">已绑定</a-tag>
<a-tag v-else color="red" class="bound-tag1">未绑定</a-tag>
</span>
</div>
<a-button v-if="item.bound" type="primary" danger class="action-btn" @click="unbind(item.name)">
<template #icon><fs-icon icon="ion:unlink-outline" /></template>
解绑
</a-button>
<a-button v-else type="primary" class="action-btn" @click="bind(item.name)">
<template #icon><fs-icon icon="ion:link-outline" /></template>
绑定
</a-button>
</div>
</template>
<div v-if="computedOauthBounds.length === 0" class="empty-text">暂无可用的第三方账号绑定</div>
</div>
<div class="passkey-info">
<div class="passkey-name">{{ passkey.deviceName }}</div>
<div class="passkey-meta flex items-center">
<span class="meta-item flex items-center">
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
{{ formatDate(passkey.registeredAt) }}
</span>
<span class="meta-item flex items-center">
<fs-icon icon="ion:time-outline" class="meta-icon" />
最近使用<fs-time-humanize :model-value="passkey.updateTime" />
</span>
</div>
</div>
<a-button type="primary" danger class="remove-btn" @click="unbindPasskey(passkey.id)">
<template #icon><fs-icon icon="ion:trash-outline" /></template>
移除
</a-button>
</div>
</div>
<div v-if="passkeys.length === 0" class="empty-state">
<fs-icon icon="ion:finger-print" class="empty-icon" />
<p class="empty-text">暂无Passkey</p>
<div class="w-full md:w-1/2 md:pl-2">
<div v-if="settingStore.sysPublic.passkeyEnabled && settingStore.isPlus" class="passkey-card md:rounded">
<div class="card-title">
<fs-icon icon="ion:finger-print" class="title-icon" />
<span>Passkey 安全密钥</span>
</div>
<div class="passkey-list">
<div v-for="passkey in passkeys" :key="passkey.id" class="passkey-item">
<div class="passkey-icon">
<fs-icon icon="ion:finger-print" class="icon" />
</div>
<div class="passkey-info">
<div class="passkey-name">{{ passkey.deviceName }}</div>
<div class="passkey-meta flex items-center">
<span class="meta-item flex items-center">
<fs-icon icon="ion:calendar-outline" class="meta-icon" />
{{ formatDate(passkey.registeredAt) }}
</span>
<span class="meta-item flex items-center">
<fs-icon icon="ion:time-outline" class="meta-icon" />
最近使用<fs-time-humanize :model-value="passkey.updateTime" />
</span>
</div>
</div>
<a-button type="primary" danger class="remove-btn" @click="unbindPasskey(passkey.id)">
<template #icon><fs-icon icon="ion:trash-outline" /></template>
移除
</a-button>
</div>
</div>
<div v-if="passkeys.length === 0" class="empty-state">
<fs-icon icon="ion:finger-print" class="empty-icon" />
<p class="empty-text">暂无Passkey</p>
</div>
<div v-if="!passkeySupported" class="warning-box">
<fs-icon icon="ion:warning-outline" class="warning-icon" />
<span>{{ t("authentication.passkeyNotSupported") }}</span>
</div>
<a-button v-if="passkeySupported" type="primary" class="add-btn" @click="registerPasskey">
<template #icon><fs-icon icon="ion:add-circle-outline" /></template>
注册新的Passkey
</a-button>
<pre class="helper">{{ t("authentication.passkeyRegisterHelper") }}</pre>
</div>
</div>
<div v-if="!passkeySupported" class="warning-box">
<fs-icon icon="ion:warning-outline" class="warning-icon" />
<span>{{ t("authentication.passkeyNotSupported") }}</span>
</div>
<a-button v-if="passkeySupported" type="primary" class="add-btn" @click="registerPasskey">
<template #icon><fs-icon icon="ion:add-circle-outline" /></template>
注册新的Passkey
</a-button>
<pre class="helper">{{ t("authentication.passkeyRegisterHelper") }}</pre>
</div>
</div>
</fs-page>
@@ -485,7 +492,7 @@ onMounted(async () => {
display: flex;
flex-direction: column;
gap: 20px;
max-width: 1000px;
// max-width: 1000px;
.profile-card,
.bindings-card,
@@ -649,7 +656,7 @@ onMounted(async () => {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #ebefff 0%, #e5d4ff 100%);
border-radius: 10px;
}
@@ -4,7 +4,7 @@
<!-- <div class="login-title">登录</div>-->
<template v-if="!isOauthOnly">
<a-tabs v-model:active-key="formState.loginType" :tab-bar-style="{ textAlign: 'center', borderBottom: 'unset' }">
<a-tab-pane key="password" :tab="t('authentication.passwordTab')" :disabled="sysPublicSettings.passwordLoginEnabled !== true">
<a-tab-pane key="password" :tab="t('authentication.passwordTab')">
<template v-if="formState.loginType === 'password'">
<!-- <div class="login-title">登录</div>-->
<a-form-item required has-feedback name="username" :rules="rules.username">
@@ -48,7 +48,7 @@
</a-tab-pane>
<a-tab-pane key="passkey" :tab="t('authentication.passkeyTab')">
<template v-if="formState.loginType === 'passkey'">
<div v-if="!passkeySupported" class="text-red-500 text-sm mt-2">
<div v-if="!passkeySupported" class="text-red-500 text-sm mt-2 text-center mb-10">
{{ t("authentication.passkeyNotSupported") }}
</div>
</template>
@@ -6,6 +6,10 @@
<a-switch v-model:checked="formState.public.passkeyEnabled" :disabled="!settingsStore.isPlus" :title="t('certd.plusFeature')" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<pre class="helper">{{ t("certd.sys.setting.passkeyEnabledHelper", [bindDomain]) }}</pre>
<div v-if="!bindDomainIsSame" class="text-red-500 text-sm mt-2">
{{ t("certd.sys.setting.passkeyHostnameNotSame") }}
</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.enableOauth')" :name="['public', 'oauthEnabled']">
<div class="flex-o">
@@ -85,7 +89,7 @@
<script setup lang="tsx">
import { notification } from "ant-design-vue";
import { merge } from "lodash-es";
import { reactive, ref, Ref } from "vue";
import { computed, reactive, ref, Ref } from "vue";
import AddonSelector from "../../../certd/addon/addon-selector/index.vue";
import { useSettingStore } from "/@/store/settings";
import * as api from "/@/views/sys/settings/api";
@@ -107,6 +111,16 @@ async function loadOauthProviders() {
oauthProviders.value = await api.GetOauthProviders();
}
const bindDomain = computed(() => {
const uri = new URL(settingsStore.installInfo.bindUrl);
return uri.hostname;
});
const bindDomainIsSame = computed(() => {
const currentHostname = window.location.hostname;
return bindDomain.value === currentHostname;
});
function fillOauthProviders(form: any) {
const providers: any = {};
for (const item of oauthProviders.value) {