UI 子路径 /ui
@tripo3d/auth/ui 是独立子路径,与 core 完全解耦:业务方可以只用 core自己写 UI;也可以用 UI 子路径拿到开箱即用的 dialog / 独立登录页 / OIDC 完成页。本页讲清楚导出面、命令式 showLogin 入口、4 个页面组件、context 注入器、i18n 11 种 locale,以及独立站点 vs 业务嵌入两种模式。
接入前置:宿主须启用 auth 构建集成
auth UI 组件不自带 CSS,所有样式靠宿主 UnoCSS 扫描;Vite 默认会预打包 node_modules,auth 一被预打包就绕过 UnoCSS → dev 模式无样式。Nuxt 添加 @tripo3d/auth/nuxt 模块;纯 Vite 使用 @tripo3d/auth/vite 的 tripoAuth():
import { tripoAuth } from '@tripo3d/auth/vite';
// 纯 Vite:vite.config 的 plugins
plugins: [tripoAuth()]Nuxt 模块默认在 SSR head 预热 Google GSI(一次 preconnect + async script),让脚本在 HTML 解析阶段就开始下载,业务无需再手写 useHead。确定用不到 Google 登录、或需要按登录态自行控制注入时,配置 tripoAuth: { preloadGoogleGsi: false } 关闭;关闭后 auth UI 仍会在实际需要 GSI 时通过 SDK loader 按需加载。
导出面
import {
configureLoginWall, // 注册运营中台登录墙配置源
preloadLoginWall, // 预取并缓存登录墙配置
resolveLoginWallSnapshot,
resolveLoginWallText,
showLogin, // 命令式:弹 LoginDialog(返回 void)
LoginPage, // 独立登录页(standalone);:auth + :complete-url 必填
OidcCompletePage, // OIDC 完成页;:auth prop 必填
OidcEdgePage, // OIDC 中间页;:auth + :complete-url 必填
DEFAULT_LOCALE, // 'en'
SUPPORTED_LOCALES, // 11 种 locale 数组(as const tuple)
} from '@tripo3d/auth/ui';
import type {
ShowLoginOptions,
LoginLocale,
LocaleInput,
LoginMethod, // 'email' | 'phone'
OidcVisibility, // Partial<Record<OidcProviderKey, boolean>>
OidcProviderKey, // 'google' | 'twitter' | 'discord' | 'apple' | 'microsoft' | 'facebook'
LoginWallConfig,
LoginWallConfigOverride,
LoginWallConfigSet,
LoginWallConfigSource,
LoginWallLocalizedText,
LoginWallPreloadResult,
LoginWallSceneConfig,
LoginWallSnapshot,
} from '@tripo3d/auth/ui';注意:
LoginDialog不直接导出——业务方走showLogin(auth, options)命令式入口(options 必填,含completeUrl)- 3 个 page 组件统一通过
:auth="auth"prop 接收TripoAuth实例;LoginPage/OidcEdgePage另需:complete-url;无 provide/inject 概念暴露给业务方 - core 类型(
TripoAuth/LoginParams/LoginSignal)从@tripo3d/auth导出;UI 子路径只暴露 UI 相关 API - 子路径依赖
@tripo3d/design的dialog.show/toast等组件,宿主必须挂<DialogOpener />
命令式 showLogin
业务任意位置一键弹出登录对话框,邮箱/密码 + OIDC 全在同一个 dialog 里完成:
import { showLogin } from '@tripo3d/auth/ui';
// showLogin 返回 void —— 登录完成统一靠 onLogin handler 收尾
showLogin(auth, {
completeUrl: 'https://www.tripo3d.ai/login/oidc', // 必填:OIDC 完成页绝对 URL
locale: 'ja', // 可选:宿主当前语言;缺省回落 'en'
loginMethod: 'phone', // 可选:'email'(默认)| 'phone',二者互斥
oidcProviders: { facebook: false }, // 可选:per-provider 显隐,缺省全显
params: { fromBanner: 'home_v2' }, // 可选
termsUrl: 'https://www.tripo3d.ai/terms', // 可选:法务链接
privacyUrl: 'https://www.tripo3d.ai/privacy', // 可选
});
// 登录完成的唯一消费路径:
auth.signal.onLogin(({ signal, context }) => {
console.log('logged in via', signal.provider, 'isRedirect=', context.isRedirect);
});签名:
| API | 签名 | 说明 |
|---|---|---|
showLogin | (auth: TripoAuth, options: ShowLoginOptions) => void | 挂载 LoginDialog;options 必填且含 completeUrl;不返回任何东西,登录完成靠 onLogin |
ShowLoginOptions | { completeUrl: string; locale?: LocaleInput; loginMethod?: LoginMethod; loginWallVariant?: string; oidcProviders?: OidcVisibility; params?: LoginParams; termsUrl?: string; privacyUrl?: string } | completeUrl 必填(绝对 URL);locale 缺省 en;loginMethod 缺省 email;loginWallVariant 只选择登录墙 UI 快照;oidcProviders 缺省全显;params 透传到 submitCode / startOidc 并写入 signal cookie;terms/privacy 不传则不渲染对应链接 |
行为:
- 内部调
dialog.show(LoginDialog, { auth, completeUrl, loginMethod, oidcProviders, params, termsUrl, privacyUrl, loginWallSnapshot });loginWallSnapshot无条件透传,其它可选项缺省不透传 LoginDialog内useConsumeSignal(auth, ...)在挂载 / 卸载时管控轮询生命周期- 收到
LoginSignal→ dialog 自己关闭;业务侧由onLoginhandler 收尾 - 用户点 ✕ → dialog 自己关闭,无副作用
- 宿主没挂
<DialogOpener />→dialog.show返回空串 → dialog 不弹出,无副作用 - 显示语言取
options.locale,缺省'en';详见下文「i18n」段
关键约束:登录完成(含 mobile + OIDC redirect 整页跳走)的唯一消费路径是 auth.signal.onLogin(handler);务必先注册再唤起。
登录墙 UI 配置
登录墙配置只影响 showLogin 挂载的登录弹框,不改变认证流程、登录方式、OIDC、GSI 或用户政策内容。它不作用于独立 LoginPage / OidcEdgePage,这两个页面始终使用内置 control。
接入时按以下顺序执行:
import {
configureLoginWall,
preloadLoginWall,
showLogin,
} from '@tripo3d/auth/ui';
configureLoginWall(auth, {
dataId: 'mc.config.schema.login-wall.json',
endpoint: 'https://api.example.com',
key: 'login_wall',
headers: { 'use-language': 'EN' },
timeoutMs: 3500,
onError: (error) => reportConfigError(error),
});
await preloadLoginWall(auth);
showLogin(auth, {
completeUrl: 'https://www.tripo3d.ai/login/oidc',
loginWallVariant: 'Style-optimization-test1',
});必须在 showLogin 之前 await preloadLoginWall 完成预取。showLogin 是同步入口;如果预取尚未完成,resolveLoginWallSnapshot 会同步返回 source: 'builtin',配置会静默回退且本次弹框不会再切换到运营配置。
dataId 仅记录 Nacos Schema 来源,不参与运行时寻址;运行时请求为 GET /v2/studio/common/config?biz=login_wall。请求默认 3500ms 超时,失败、超时或响应不合法时回退内置 control;失败后再次调用 preloadLoginWall 会重新请求。需要鉴权头、自定义重试或其它请求行为的宿主通过 headers / fetchApi 注入;配置源也可通过 onError 接收失败通知,未提供时 SDK 使用 console.warn。
loginWallVariant 只选择已预取的 UI 快照。未传时使用 base,未知 variant 回退 base;配置允许的内容仅包括左侧视觉显隐、图片、品牌/场景文案、Form 标题和副标题。
登录方式与 OIDC 显隐(loginMethod / oidcProviders)
showLogin / LoginPage / LoginDialog 共享两个可选 prop,用来定制登录表单:
loginMethod:邮箱 / 手机号互斥
'email'(默认)—— 验证码 / 密码双 tab + 邮箱输入(历史行为)'phone'—— 仅手机号验证码(无密码、无忘记密码),区号固定+86(仅中国大陆)
两种方式互斥:同一时刻只渲染一套凭据区。手机号链路底层走 core 的 sendCode({ phone }) / submitCode({ code, phone }) 重载(详见 验证码流程 · 手机号验证码)。
showLogin(auth, { completeUrl, loginMethod: 'phone' });<LoginPage :auth="auth" complete-url="..." login-method="phone" />oidcProviders:per-provider 显隐开关
类型 OidcVisibility = Partial<Record<OidcProviderKey, boolean>>,按 provider 名控制显隐:缺省 / true 显示,false 隐藏。保持原有顺序与「google 主按钮 + 其余圆形次按钮」布局,不改主次;全部隐藏时连「Or」分隔线一起不渲染。
// 只隐藏 facebook 与 microsoft,其余照常显示
showLogin(auth, { completeUrl, oidcProviders: { facebook: false, microsoft: false } });
// 隐藏全部 OIDC(只留验证码 / 密码登录)
showLogin(auth, {
completeUrl,
oidcProviders: { google: false, twitter: false, discord: false, apple: false, microsoft: false, facebook: false },
});OidcProviderKey = 'google' | 'twitter' | 'discord' | 'apple' | 'microsoft' | 'facebook'(按 ory provider 名,twitter 即 X)。
4 个页面组件
| 组件 | 路由约定 | 职责 |
|---|---|---|
LoginPage | /login(独立登录页) | :auth + :complete-url 必填,:login-method / :oidc-providers / :terms-url / :privacy-url 可选;渲染大字标题 + 4 个 feature 胶囊 + 内层登录表单(内部组件 LoginShell / LoginPanel,不对外);通过 useConsumeSignal 收到 LoginSignal 后 location.assign(returnTo ?? '/') |
OidcCompletePage | completeUrl 所在路由(如 /login/oidc) | :auth 必填;onMounted 调 auth.oidc.handleComplete();只渲染 spinner 兜底,handleComplete 内部已自动 close 或 redirect |
OidcEdgePage | edge 路由(由 ory 服务端配置,如 /login/oidc/edge?flow=...) | :auth + :complete-url 必填;onMounted 调 auth.oidc.loadEdgeFlow(flowId, { completeUrl });按 flow.kind 渲染 continue / merge 表单(provider 由 SDK 内部从 ory 实时读出) |
LoginDialog(不直接导出) | 由 showLogin 内部挂载 | 内层登录表单的 dialog 包装;收到 LoginSignal 自行关闭 |
LoginPage 接入(独立 site)
3 个 page 组件统一通过 :auth prop 拿到 TripoAuth 实例。最朴素的做法是为每条路由写一个薄薄的 wrapper SFC:
<!-- LoginRoute.vue -->
<script setup lang="ts">
import { LoginPage } from '@tripo3d/auth/ui';
import { auth } from '@/composables/auth'; // 业务方在 plugin / composable 里建好的 TripoAuth 实例
</script>
<template>
<!-- complete-url 必填(绝对 URL);login-method / oidc-providers / terms / privacy 可选 -->
<LoginPage
:auth="auth"
complete-url="https://www.tripo3d.ai/login/oidc"
login-method="email"
:oidc-providers="{ facebook: false }"
terms-url="https://www.tripo3d.ai/terms"
privacy-url="https://www.tripo3d.ai/privacy"
/>
</template>router 配置:
import LoginRoute from './LoginRoute.vue';
import OidcCompleteRoute from './OidcCompleteRoute.vue';
import OidcEdgeRoute from './OidcEdgeRoute.vue';
routes: [
{ path: '/login', component: LoginRoute },
{ path: '/login/oidc', component: OidcCompleteRoute },
{ path: '/login/oidc/edge', component: OidcEdgeRoute },
]或者直接在 router 里用 h() 包一层:
import { h } from 'vue';
import { LoginPage, OidcCompletePage, OidcEdgePage } from '@tripo3d/auth/ui';
import { auth } from '@/composables/auth';
const completeUrl = 'https://www.tripo3d.ai/login/oidc';
routes: [
{ path: '/login', component: { render: () => h(LoginPage, { auth, completeUrl }) } },
{ path: '/login/oidc', component: { render: () => h(OidcCompletePage, { auth }) } },
{ path: '/login/oidc/edge', component: { render: () => h(OidcEdgePage, { auth, completeUrl }) } },
]LoginPage 内部从 query 读 returnTo(同站校验通过的部分)、locale、params:
https://www.tripo3d.ai/login?returnTo=https%3A%2F%2Fstudio.tripo3d.ai%2Fprojects&locale=zh¶ms=eyJzb3VyY2UiOiJob21lX3YyIn0returnTo跨站会被 drop(useLoginEnvironment的isSameSiteTripo校验)params是 base64url,组件内部用decodeLoginParams还原后通过provideLoginParams注入给 LoginForm,后者透传给submitCode / startOidclocale直接 provide 给 i18n composable
OidcCompletePage 接入
OidcCompletePage 内部已完全把 handleComplete() 挂到 onMounted,业务方只需挂载并传 :auth:
<OidcCompletePage :auth="auth" />错误兜底:handleComplete 抛错(罕见,通常是 URL mode / provider 字段不合法)→ toast error.message → 2 秒后回 returnTo ?? '/login'。
OidcEdgePage 接入
<!-- complete-url 必填(绝对 URL):merge 成功后跳的 OIDC 完成页 -->
<OidcEdgePage :auth="auth" complete-url="https://www.tripo3d.ai/login/oidc" />内部把 prop complete-url 透传给 loadEdgeFlow(flowId, { completeUrl })——edge 页 URL 只携带 flow id;mode 不传,由 SDK 内部按 window.opener 探测(popup 内有 / redirect 整页跳过来没有);provider 由 SDK 内部从 ory flow.ui.nodes 实时读出,调用方无需传:
completeUrl:merge 分支成功后 SDK 把页面 replace 到这里,再由handleComplete收尾provider真实归因:OidcEdgeForm内flow.provider暴露给业务(如要做埋点);merge 分支跳完成页时也用真实 provider 拼 URL
如果业务方需要自定义 edge 页(如改 UI 文案 / 加埋点):
<script setup lang="ts">
import { onMounted, shallowRef } from 'vue';
import type { EdgeFlow, TripoAuth } from '@tripo3d/auth';
const props = defineProps<{ auth: TripoAuth }>();
const flow = shallowRef<EdgeFlow | null>(null);
onMounted(async () => {
const url = new URL(window.location.href);
const flowId = url.searchParams.get('flow');
if (!flowId) return;
flow.value = await props.auth.oidc.loadEdgeFlow(flowId, {
completeUrl: 'https://www.tripo3d.ai/login/oidc', // 必填,绝对 URL
// mode? params? returnTo? locale? 都可选;mode 不传 SDK 按 window.opener 探测
});
// 拿到真实 provider 做埋点
console.log('edge provider:', flow.value.provider);
});
</script>auth prop 接入约定
3 个 page 组件 + showLogin 都通过显式参数收 TripoAuth:
showLogin(auth, options)—— 第一参传实例,options 必填含completeUrl<LoginPage :auth :complete-url :terms-url? :privacy-url? />/<OidcCompletePage :auth />/<OidcEdgePage :auth :complete-url />—— prop 传实例;complete-url必填项见上文表格
SDK 内部 page 组件收到 prop 后会用 Vue provide 在自己的子树里注入一份,给 LoginPanel 下的登录步骤(如 LoginForm / ResetFlow)及 OidcEdgeForm 等子组件读。这个 provide/inject 是纯内部的:
- key 是 SDK 私有
Symbol useAuthContextcomposable 不对外暴露- 业务自定义子组件想拿
auth应直接从自家 plugin / store 取,不要尝试 inject SDK 的 key
这样设计的理由:业务方面对 SDK 的接入面只有「构造 + 4 个组件」,零 composable 概念,跟 <LoginDialog> 内嵌走的也是同一套 prop 风格。
i18n(11 种 locale)
UI 子路径内置 11 种 locale:
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '@tripo3d/auth/ui';
console.log(SUPPORTED_LOCALES);
// ['en', 'zh', 'es', 'ko', 'ru', 'pt', 'ja', 'tr', 'de', 'fr', 'it'] as const
console.log(DEFAULT_LOCALE); // 'en'类型:
import type { LocaleInput, LoginLocale } from '@tripo3d/auth/ui';
// LoginLocale = 'en' | 'zh' | 'es' | 'ko' | 'ru' | 'pt' | 'ja' | 'tr' | 'de' | 'fr' | 'it'
// LocaleInput = LoginLocale | (string & {}) // 接受任意 string;SDK 内部归一化locale 来源(按入口区分)
showLogin(原地弹窗) —— 唯一显式接受 locale 的入口:
showLogin(auth, {
completeUrl: 'https://www.tripo3d.ai/login/oidc',
locale: 'ja', // 可选;缺省回落 'en'
});宿主把当前 i18n locale(如 Nuxt 的 useI18n().locale.value)原样传进来即可,SDK 内部用 resolveLocale 归一化:'pt-BR' → 'pt'、'ja-JP' → 'ja'、不在 11 种支持语言里的回落 'en'。
LoginPage / OidcCompletePage / OidcEdgePage(独立登录页 + OIDC 落地页) —— SDK 自理,不接受 locale prop:
- 优先读 URL
?locale=,缺失回落'en'。 - 这条
?locale=是 SDK 的私有信道:startOidc({ locale })让 core 通过buildCompleteUrl把?locale=写到 OIDC 回跳 URL 上,完成页 / edge 页再读回。整条 OIDC 跳转链显示语言自动延续,业务方不用每个落地页都把 locale 当 prop 传一遍。 - 直接访问独立登录页时在 URL 上拼
?locale=zh同样生效。
链路示意
[业务方] showLogin(auth, { locale: 'ja' })
│
▼ 用户点 Google
LoginForm.startOidc({ locale: 'ja' })
│
▼ core 把 ?locale=ja 写进回跳 URL
https://www.tripo3d.ai/login/oidc?...&locale=ja
│
▼ ory 回跳到完成页
OidcCompletePage 读 ?locale=ja → 显示日语注意:v2.0.1 起 showLogin 已移除 navigator 自动检测;dialog 缺省回落 'en',宿主必须显式传 locale 才能本地化。三个 page 组件没有 locale prop——业务方需要在独立站点强制语言时,在路由 URL 上拼 ?locale=xx(或让自家入口链接默认带上)。
LoginParams 编解码
encodeLoginParams / decodeLoginParams 从 core 导出(注意是 core,不是 ui):
import { decodeLoginParams, encodeLoginParams } from '@tripo3d/auth';
// 编:业务想自己拼独立登录页 URL 时
const encoded = encodeLoginParams({ source: 'home_v2', utm: 'x' });
const url = `https://www.tripo3d.ai/login?params=${encoded}`;
// 解:业务想从自家 SSR 拿到 URL 后还原原值
const params = decodeLoginParams(url.searchParams.get('params'));要点:
- 非 JSON-serializable 输入(function / symbol)编码会返回
undefined;不抛错 - base64url 串在 URL 里安全,无需再
encodeURIComponent(searchParams.set会自动做一次 URL 安全编码) - 解码失败返回
undefined,不抛错
独立站点 vs 业务嵌入
独立站点(standalone)
www.tripo3d.ai 承载 /login /login/oidc /login/oidc/edge 三条路由;业务子域(studio.tripo3d.ai)通过 OIDC redirect 把用户送过去:
studio.tripo3d.ai → showLogin / startOidc → SDK 推 redirect
→ location.assign('https://www.tripo3d.ai/login?returnTo=https://studio.tripo3d.ai/projects')
→ 用户在 www 完成 OIDC
→ www handleComplete 写 cookie → location.assign(returnTo)
→ studio 冷启动 → 构造时 pendingSignals 兜底 → onLogin handler接入:
www侧:LoginPage+OidcCompletePage+OidcEdgePage全挂,每个组件:auth="auth"显式传实例,LoginPage/OidcEdgePage另传:complete-urlstudio侧:实例化TripoAuth+ 注册onLoginhandler;不需要任何 UI 组件
业务嵌入(dialog)
业务子域自己挂 <DialogOpener />,直接 showLogin(auth, { completeUrl }):
studio.tripo3d.ai → showLogin → LoginDialog 内嵌
PC:startOidc → SDK 推 popup → window.open(...) → 弹窗完成 → onLogin handler
Mobile:startOidc → SDK 推 redirect → location.assign(...) → 回业务页 → onLogin handlerOIDC 仍然要回到业务方传入的 completeUrl(业务方仍然需要在该 URL 所在站点跑一份 OidcCompletePage)。
完整 Vue 应用接入示例
// composables/auth.ts —— 单例实例 + 全局 onLogin handler
import { TripoAuth } from '@tripo3d/auth';
export const auth = new TripoAuth({
basePath: 'https://auth.tripo3d.ai',
});
// SDK 已在派发前 await token.refresh() 兜底,handler 跑起来 token 直接可用
auth.signal.onLogin(async ({ signal, context }) => {
await fetchCurrentUser();
});// main.ts
import { createApp } from 'vue';
import { setup as setupDesign } from '@tripo3d/design';
import App from './App.vue';
const app = createApp(App);
setupDesign(app); // 注册 <DialogOpener /> 等全局组件
app.mount('#app');业务任意组件里直接 import auth 和 showLogin:
import { showLogin } from '@tripo3d/auth/ui';
import { auth } from '@/composables/auth';
showLogin(auth, { completeUrl: 'https://www.tripo3d.ai/login/oidc' });独立登录页通过 prop 显式传实例(路由配置见上文)。
下一步
- 架构总览 —
TripoAuth与 6 个命名空间全景 - OIDC 登录 —
OidcCompletePage/OidcEdgePage背后的 SDK 调用 - 登录信号 (cookie) —
useConsumeSignal双通道机制 - 错误处理 —
showLogin/ 页面组件的兜底 toast 路径