feat(auth): 新增微信聚合登录入口与账号绑定卡\n\n- OAuthButtons.vue: aggregate 启用时渲染微信登录按钮,微信绿 hover + 专属 SVG 图标\n- OAuthBindingCard.vue: 新增微信 aggregate 绑定卡,支持绑定跳转 /api/auth/aggregate 和解绑\n- account/index.vue: hasOAuthProviders 判定加入 aggregate\n- oauth.ts: getProviderDisplayName 中 aggregate 映射为 微信
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 2m8s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Successful in 10m3s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Successful in 6s

This commit is contained in:
2026-09-12 21:06:26 +08:00
parent a937f32e6e
commit 467dd38d5f
4 changed files with 49 additions and 8 deletions
+40
View File
@@ -117,6 +117,45 @@
</button> </button>
</div> </div>
<!-- 微信 (聚合登录 aggregate) -->
<div v-if="config.public.oauth.aggregate" :class="itemClass">
<div class="flex items-center gap-4">
<div
class="w-10 h-10 rounded-xl bg-green-500/10 flex items-center justify-center border border-green-500/20 text-green-500"
>
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true">
<path d="M8.69 3.5C5.06 3.5 2.12 6.03 2.12 9.14c0 1.79 1.04 3.37 2.66 4.41l-.68 2.05 2.56-1.28c.52.11 1.06.17 1.61.17h.51c-.08-.36-.11-.73-.08-1.09H8.69c-2.59 0-4.69-1.93-4.69-4.31s2.1-4.31 4.69-4.31h1.37c.25-.98 1.09-1.7 2.09-1.7.43 0 .83.13 1.17.36C12.61 3.06 10.76 3.5 8.69 3.5zM10.4 5.62c-.93 0-1.69.76-1.69 1.69s.76 1.69 1.69 1.69 1.69-.76 1.69-1.69-.76-1.69-1.69-1.69zm7.19.91c-2.13 0-3.86 1.7-3.86 3.8s1.73 3.8 3.86 3.8c.5 0 1.02-.1 1.47-.27L21.5 15l-.67-1.95c1.21-.88 2-2.24 2-3.72 0-2.1-1.73-3.8-3.84-3.8-.1 0-.2 0-.4.01zm-1.84 2.69c-.66 0-1.2.54-1.2 1.2s.54 1.2 1.2 1.2 1.2-.54 1.2-1.2-.54-1.2-1.2-1.2z"/>
</svg>
</div>
<div class="flex flex-col">
<span class="text-sm font-bold text-zinc-200">{{
getProviderDisplayName('aggregate')
}}</span>
<span v-if="aggregateIdentity" class="text-[11px] text-blue-500 font-medium mt-0.5">{{
aggregateIdentity.providerUsername
}}</span>
<span v-else class="text-[11px] text-zinc-500 mt-0.5">未绑定</span>
</div>
</div>
<button
v-if="aggregateIdentity"
class="px-4 py-1.5 bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 text-rose-500 text-xs font-black rounded-xl transition-all disabled:opacity-50"
:disabled="actionLoading"
@click="confirmUnbind('aggregate')"
>
{{ actionLoading ? '处理中...' : '解绑' }}
</button>
<button
v-else
class="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white text-xs font-black rounded-xl shadow-lg shadow-emerald-900/20 transition-all active:scale-95 disabled:opacity-50"
:disabled="actionLoading"
@click="handleBind('aggregate')"
>
{{ actionLoading ? '跳转中...' : '立即绑定' }}
</button>
</div>
<!-- WebAuthn / Passkey --> <!-- WebAuthn / Passkey -->
<div <div
v-if="isWebAuthnSupported || webauthnIdentities.length > 0 || !isSecureContext" v-if="isWebAuthnSupported || webauthnIdentities.length > 0 || !isSecureContext"
@@ -350,6 +389,7 @@ const itemClass =
const githubIdentity = computed(() => identities.value.find((i) => i.provider === 'github')) const githubIdentity = computed(() => identities.value.find((i) => i.provider === 'github'))
const casdoorIdentity = computed(() => identities.value.find((i) => i.provider === 'casdoor')) const casdoorIdentity = computed(() => identities.value.find((i) => i.provider === 'casdoor'))
const googleIdentity = computed(() => identities.value.find((i) => i.provider === 'google')) const googleIdentity = computed(() => identities.value.find((i) => i.provider === 'google'))
const aggregateIdentity = computed(() => identities.value.find((i) => i.provider === 'aggregate'))
const webauthnIdentities = computed(() => identities.value.filter((i) => i.provider === 'webauthn')) const webauthnIdentities = computed(() => identities.value.filter((i) => i.provider === 'webauthn'))
const fetchIdentities = async () => { const fetchIdentities = async () => {
+6 -6
View File
@@ -27,12 +27,12 @@
<button <button
v-if="config.public.oauth.aggregate" v-if="config.public.oauth.aggregate"
type="button" type="button"
class="oauth-btn oauth-btn-aggregate" class="oauth-btn oauth-btn-wechat"
title="使用 QQ / 微信登录" title="使用微信登录"
@click="loginWith('aggregate')" @click="loginWith('aggregate')"
> >
<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"> <svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true">
<path d="M8.5 4C4.9 4 2 6.8 2 10.3c0 2 1.1 3.7 2.8 4.9L4 18l3-1.5c.5.1 1 .2 1.5.2h.6c-.1-.4-.1-.8 0-1.2H8.5C5.5 15.5 3 13.2 3 10.3 3 7.4 5.5 5 8.5 5h1.6c.3-1.1 1.3-1.9 2.5-1.9.5 0 1 .2 1.4.5C13.3 3 10.7 4 8.5 4zM10.1 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm8.4 1.2c-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5c.6 0 1.2-.1 1.7-.3L22 17.5l-.8-2.3c1.4-1 2.3-2.6 2.3-4.3C23.5 9.2 21.5 7.2 19 7.2c-.5 0-1 0-1.4.1C16.9 7.8 17.9 7.2 19 7.2zM17 9.7c-.8 0-1.5.7-1.5 1.5s.7 1.5 1.5 1.5 1.5-.7 1.5-1.5-.7-1.5-1.5-1.5z"/> <path d="M8.69 3.5C5.06 3.5 2.12 6.03 2.12 9.14c0 1.79 1.04 3.37 2.66 4.41l-.68 2.05 2.56-1.28c.52.11 1.06.17 1.61.17h.51c-.08-.36-.11-.73-.08-1.09H8.69c-2.59 0-4.69-1.93-4.69-4.31s2.1-4.31 4.69-4.31h1.37c.25-.98 1.09-1.7 2.09-1.7.43 0 .83.13 1.17.36C12.61 3.06 10.76 3.5 8.69 3.5zM10.4 5.62c-.93 0-1.69.76-1.69 1.69s.76 1.69 1.69 1.69 1.69-.76 1.69-1.69-.76-1.69-1.69-1.69zm7.19.91c-2.13 0-3.86 1.7-3.86 3.8s1.73 3.8 3.86 3.8c.5 0 1.02-.1 1.47-.27L21.5 15l-.67-1.95c1.21-.88 2-2.24 2-3.72 0-2.1-1.73-3.8-3.84-3.8-.1 0-.2 0-.4.01zm-1.84 2.69c-.66 0-1.2.54-1.2 1.2s.54 1.2 1.2 1.2 1.2-.54 1.2-1.2-.54-1.2-1.2-1.2z"/>
</svg> </svg>
</button> </button>
</div> </div>
@@ -152,10 +152,10 @@ const loginWith = (provider: string) => {
border-color: #dadce0; border-color: #dadce0;
} }
.oauth-btn-aggregate:hover { .oauth-btn-wechat:hover {
background: var(--bg-secondary, #12b7f5); background: var(--bg-secondary, #07c160);
color: var(--text-primary, #ffffff); color: var(--text-primary, #ffffff);
border-color: var(--border-secondary, #12b7f5); border-color: var(--border-secondary, #07c160);
} }
.oauth-empty-hint { .oauth-empty-hint {
+1 -1
View File
@@ -133,7 +133,7 @@ const { showToast } = useToast()
const config = useRuntimeConfig() const config = useRuntimeConfig()
const hasOAuthProviders = computed(() => { const hasOAuthProviders = computed(() => {
return config.public.oauth.github || config.public.oauth.casdoor || config.public.oauth.google return config.public.oauth.github || config.public.oauth.casdoor || config.public.oauth.google || config.public.oauth.aggregate
}) })
const avatarError = ref(false) const avatarError = ref(false)
+2 -1
View File
@@ -2,7 +2,8 @@ export const getProviderDisplayName = (provider: string): string => {
const map: Record<string, string> = { const map: Record<string, string> = {
github: 'GitHub', github: 'GitHub',
casdoor: 'Casdoor', casdoor: 'Casdoor',
google: 'Google' google: 'Google',
aggregate: '微信'
} }
return map[provider.toLowerCase()] || provider.charAt(0).toUpperCase() + provider.slice(1) return map[provider.toLowerCase()] || provider.charAt(0).toUpperCase() + provider.slice(1)
} }