Skip to content

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():

ts
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 按需加载。

详见 5 分钟接入 · 1.5 宿主构建集成。

导出面 ​

ts
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 里完成:

ts
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 自己关闭;业务侧由 onLogin handler 收尾
  • 用户点 ✕ → dialog 自己关闭,无副作用
  • 宿主没挂 <DialogOpener /> → dialog.show 返回空串 → dialog 不弹出,无副作用
  • 显示语言取 options.locale,缺省 'en';详见下文「i18n」段

关键约束:登录完成(含 mobile + OIDC redirect 整页跳走)的唯一消费路径是 auth.signal.onLogin(handler);务必先注册再唤起。

登录墙 UI 配置 ​

登录墙配置只影响 showLogin 挂载的登录弹框,不改变认证流程、登录方式、OIDC、GSI 或用户政策内容。它不作用于独立 LoginPage / OidcEdgePage,这两个页面始终使用内置 control。

接入时按以下顺序执行:

ts
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 }) 重载(详见 验证码流程 · 手机号验证码)。

ts
showLogin(auth, { completeUrl, loginMethod: 'phone' });
vue
<LoginPage :auth="auth" complete-url="..." login-method="phone" />

oidcProviders:per-provider 显隐开关 ​

类型 OidcVisibility = Partial<Record<OidcProviderKey, boolean>>,按 provider 名控制显隐:缺省 / true 显示,false 隐藏。保持原有顺序与「google 主按钮 + 其余圆形次按钮」布局,不改主次;全部隐藏时连「Or」分隔线一起不渲染。

ts
// 只隐藏 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 ?? '/')
OidcCompletePagecompleteUrl 所在路由(如 /login/oidc):auth 必填;onMounted 调 auth.oidc.handleComplete();只渲染 spinner 兜底,handleComplete 内部已自动 close 或 redirect
OidcEdgePageedge 路由(由 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:

vue
<!-- 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 配置:

ts
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() 包一层:

ts
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&params=eyJzb3VyY2UiOiJob21lX3YyIn0
  • returnTo 跨站会被 drop(useLoginEnvironment 的 isSameSiteTripo 校验)
  • params 是 base64url,组件内部用 decodeLoginParams 还原后通过 provideLoginParams 注入给 LoginForm,后者透传给 submitCode / startOidc
  • locale 直接 provide 给 i18n composable

OidcCompletePage 接入 ​

OidcCompletePage 内部已完全把 handleComplete() 挂到 onMounted,业务方只需挂载并传 :auth:

vue
<OidcCompletePage :auth="auth" />

错误兜底:handleComplete 抛错(罕见,通常是 URL mode / provider 字段不合法)→ toast error.message → 2 秒后回 returnTo ?? '/login'。

OidcEdgePage 接入 ​

vue
<!-- 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 文案 / 加埋点):

vue
<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
  • useAuthContext composable 不对外暴露
  • 业务自定义子组件想拿 auth 应直接从自家 plugin / store 取,不要尝试 inject SDK 的 key

这样设计的理由:业务方面对 SDK 的接入面只有「构造 + 4 个组件」,零 composable 概念,跟 <LoginDialog> 内嵌走的也是同一套 prop 风格。

i18n(11 种 locale) ​

UI 子路径内置 11 种 locale:

ts
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'

类型:

ts
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 的入口:

ts
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):

ts
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-url
  • studio 侧:实例化 TripoAuth + 注册 onLogin handler;不需要任何 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 handler

OIDC 仍然要回到业务方传入的 completeUrl(业务方仍然需要在该 URL 所在站点跑一份 OidcCompletePage)。

完整 Vue 应用接入示例 ​

ts
// 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();
});
ts
// 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:

ts
import { showLogin } from '@tripo3d/auth/ui';
import { auth } from '@/composables/auth';

showLogin(auth, { completeUrl: 'https://www.tripo3d.ai/login/oidc' });

独立登录页通过 prop 显式传实例(路由配置见上文)。

下一步 ​

基于 MIT 协议发布(内部使用)