@csntgao/uni-base 0.1.5 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CLAUDE.md +5 -3
- package/README.md +16 -1
- package/package.json +1 -1
- package/packages/web-components/package.json +1 -1
- package/packages/web-components/src/account-context.ts +142 -5
- package/packages/web-components/src/organization-onboarding.ts +56 -26
- package/packages/web-components/src/organization-selector.ts +1 -1
- package/packages/web-components/tests/account-context.test.ts +117 -0
- package/packages/web-components/tests/organization-selector.test.ts +4 -0
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +45 -15
package/AGENTS.md
CHANGED
|
@@ -37,7 +37,7 @@ examples/vanilla 原生 HTML / JavaScript 接入示例
|
|
|
37
37
|
|
|
38
38
|
`<uniplat-user-login>` 必须严格复刻亲亲企服 40.1 注册登录原型,提供密码、验证码和扫码三个固定页签、逐字段校验错误态及注册成功过渡页。验证码登录只包含手机号和短信验证码,不得因为接口现状增加图形验证码或其他原型中不存在的字段;旧版 `createCaptcha()` 仅为源码兼容保留,组件不得调用。自定义 Host transport 的密码与扫码方法保持可选,未提供时对应页签保持显示但禁用。UniplatBase 专用 transport 已复用短信验证码及统一 `login-application` 接口提供三种登录能力;组件本身不得保存 JWT,登录结果必须先交给宿主 `setAccessToken()`,再派发不含凭据的成功事件。后端明确返回新用户时显示 2 秒过渡页,并通过 `onboarding-requested` 请求 Host 调用 `<uniplat-account-context>.startOrganizationOnboarding()`。
|
|
39
39
|
|
|
40
|
-
组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding
|
|
40
|
+
组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding>`。无归属用户默认必须先入驻:账户投影为零归属时组件用原生 `dialog.showModal()` 自动打开强制态创建流程,不渲染跳过、无归属提示与关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭,用户只能创建组织或点“退出登录”派发 `logout-requested`;企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。Host 可用 `organizationOnboarding.mandatory: false` 退回原型 4.1 的可跳过行为。强制态必须使用 top layer 对话框,不得只依赖可能被 Host `transform/overflow` 限制的固定遮罩。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 Host 派发 `organization-create-requested` 或 `create-organization-requested`。真实 UniplatBase transport 复用 `createUniplatBaseOrganizationOnboardingTransport()` 与 `system.org/create_organization`。邀请码加入、企业认证、团队升级和邀请成员等未展开页面仍派发业务事件,由 Host 实现。
|
|
41
41
|
|
|
42
42
|
`<uniplat-organization-selector>` 覆盖 40.1 原型 S.1 多归属选择弹窗,显示 Host 提供的完整企业/团队列表、类型、认证状态、角色、成员数和最近进入信息,并自行负责脱敏账户摘要、名称与统一社会信用代码搜索、全部/企业/团队类型筛选、“最近进入 / 全部组织”分组、默认选中和带目标名称的主按钮。选中与记住偏好只在组件内临时维护,点击“进入”后交由 Host 完成登录态切换,“退出登录”只派发事件。`presentation: 'modal'` 时组件自渲染 S.1 强制态弹窗:固定遮罩、无关闭按钮,遮罩点击与 `Escape` 都不关闭,`Escape` 也不冒泡到 Host 的全局关闭逻辑;默认 `inline` 只输出卡片,遮罩归 Host。S.0 的“弹不弹”判定由纯函数 `resolveOrganizationEntryDecision()` 提供,只按 0 个归属、1 个归属、命中记住选择、2 个及以上分别返回 `personal | auto-enter | prompt`;它不读写宿主存储,记住偏好由 Host 自行持久化并回传,记住的组织已不在列表时退回 `prompt`。
|
|
43
43
|
|
|
@@ -324,10 +324,12 @@ userMenu.configure({
|
|
|
324
324
|
```ts
|
|
325
325
|
accountContext.configure({
|
|
326
326
|
applicationName,
|
|
327
|
-
organizationOnboarding: { transport },
|
|
327
|
+
organizationOnboarding: { transport, mandatory },
|
|
328
328
|
})
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
+
`mandatory` 默认 `true`,非布尔值一律拒绝。
|
|
332
|
+
|
|
331
333
|
transport 只提供 `createEnterprise()` 和 `createTeam()`;稳定冲突错误使用 `OrganizationOnboardingTransportError`,不得把原始响应、JWT 或内部异常透传给组件。`createEnterprise()` 接收组织名称、企业名称与统一社会信用代码,`createTeam()` 只接收团队名称。创建成功响应只包含稳定 ID、展示名称和 `enterprise | team` 类型。
|
|
332
334
|
|
|
333
335
|
真实 UniplatBase 对接使用 `createUniplatBaseOrganizationOnboardingTransport({ serviceBaseUrl, getAccessToken })`,复用 `system.org/create_organization`。每次创建前必须重新取 JWT,并只放入 `Authorization: Bearer` header;请求不得携带调用方身份字段。只对白名单冲突码做字段级映射,`INVALID_ARGUMENT`、`ORGANIZATION_CREATE_FAILED` 和所有非协议异常统一收敛为 `UNKNOWN`。
|
|
@@ -435,7 +437,7 @@ npm 包名、导出类型名、Custom Element 标签、`configure()` 结构、
|
|
|
435
437
|
- Account Context 内嵌工作台需测试不注册独立应用切换标签、JWT 只交给 transport、HTTP Authorization header、入口净化、当前组织级应用加 `application_scope=open` 应用的筛选、缺字段时 `oid=0` 兼容、`issue_ticket` 空请求体、签发响应来源进入新 Fragment、公共事件无 JWT/Ticket、Ticket 只进入局部导航 URL、目标页面先清 Fragment 再按初始化应用登录、响应应用绑定校验、加载错误、仅请求应用列表和应用选择交互。
|
|
436
438
|
- User Login 需测试组件注册与导出、三个固定页签、不可用能力禁用、原型中不存在图形验证码且不调用兼容方法、逐字段失焦校验和协议错误态、稳定后端错误码映射、短信倒计时、密码登录、扫码待确认/已扫码/过期/成功状态、注册成功过渡页、2 秒自动入驻和立即开始、页签切换与断开后的定时器清理、登录回调,以及 JWT、密码和二维码 challenge 不进入 URL、DOM、公共事件、日志或安全错误。
|
|
437
439
|
- User Menu 需测试组件注册与导出、40.1 6.7 原型关键内容、个人账号与当前组织员工信息入口、企业认证与团队样式、安全头像与文字回退、Host 受控选中状态、重新配置、空组织状态、完整手机号拒绝、创建与退出事件、普通文本插值,以及公共事件只包含必要的组织 ID,不包含显示名、手机号、组织名称、头像 URL、JWT 或内部配置。
|
|
438
|
-
- 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT
|
|
440
|
+
- 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT 或原始异常。强制入驻还需测试零归属投影自动打开 top layer 对话框、不渲染跳过与无归属提示、`Escape` 与外部点击都不关闭、“退出登录”只派发空 detail、表单页“返回”回到创建方式选择、成功页动作才关闭对话框,以及 `mandatory: false` 保持原可跳过行为。真实 UniplatBase transport 还必须测试每次请求重新取 JWT、JWT 只进入 Authorization header、企业/团队请求字段、成功响应净化和未知错误收敛。
|
|
439
441
|
- Organization Selector 需测试完整列表、企业认证与团队类型样式、脱敏账户摘要与完整手机号拒绝、按名称和统一社会信用代码搜索且代码不进入 DOM、类型筛选与常显计数、“最近进入 / 全部组织”分组与默认选中、带目标名称的主按钮、无匹配与空归属状态、记住偏好、创建/进入/退出登录事件、`modal` 强制态无关闭按钮且遮罩点击与 `Escape` 都不关闭并在移除后解除 `Escape` 拦截、非法 `presentation` 与重复 ID 拒绝和普通文本插值;事件只包含稳定组织 ID 与偏好,不含姓名、手机号、组织名称或信用代码。`resolveOrganizationEntryDecision()` 需测试 0 个归属、单一归属、命中与失效的记住选择、2 个及以上的默认选中项和非法输入拒绝。
|
|
440
442
|
- Organization Multi Employee Selector 需测试部门树、员工分页和搜索、初始选择补取、跨部门按员工 ID 去重、多选上限、受保护项、确认/取消事件、独立滚动、移动视图、错误重试、身份切换清理,以及 JWT、完整手机号、身份字段和后端错误不进入 DOM、事件、URL或日志。专用 transport 必须测试每次请求重新读取 JWT、只使用 Authorization header、组织级 directory scope 和无调用方身份参数。
|
|
441
443
|
- Organization Single Employee Selector 需测试独立标签与导出、初始单个 ID 回显、新选择替换旧选择、未选时禁止确认、受保护项、标量确认 detail、拒绝多选打开参数,以及与多选组件相同的数据和认证安全边界。
|
package/CLAUDE.md
CHANGED
|
@@ -37,7 +37,7 @@ examples/vanilla 原生 HTML / JavaScript 接入示例
|
|
|
37
37
|
|
|
38
38
|
`<uniplat-user-login>` 必须严格复刻亲亲企服 40.1 注册登录原型,提供密码、验证码和扫码三个固定页签、逐字段校验错误态及注册成功过渡页。验证码登录只包含手机号和短信验证码,不得因为接口现状增加图形验证码或其他原型中不存在的字段;旧版 `createCaptcha()` 仅为源码兼容保留,组件不得调用。自定义 Host transport 的密码与扫码方法保持可选,未提供时对应页签保持显示但禁用。UniplatBase 专用 transport 已复用短信验证码及统一 `login-application` 接口提供三种登录能力;组件本身不得保存 JWT,登录结果必须先交给宿主 `setAccessToken()`,再派发不含凭据的成功事件。后端明确返回新用户时显示 2 秒过渡页,并通过 `onboarding-requested` 请求 Host 调用 `<uniplat-account-context>.startOrganizationOnboarding()`。
|
|
39
39
|
|
|
40
|
-
组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding
|
|
40
|
+
组织入驻不是公开 Web Component。40.1 原型 02、A/B 企业/团队创建分支、校验/成功状态及 4.1 无归属提示均由 `<uniplat-account-context>` 内部实现;Host 在其 `organizationOnboarding.transport` 中注入创建能力,不能使用或注册 `<uniplat-organization-onboarding>`。无归属用户默认必须先入驻:账户投影为零归属时组件用原生 `dialog.showModal()` 自动打开强制态创建流程,不渲染跳过、无归属提示与关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭,用户只能创建组织或点“退出登录”派发 `logout-requested`;企业/团队表单页提供“返回”回到创建方式选择,创建成功页的认证、升级或邀请动作才是关闭对话框的终点。Host 可用 `organizationOnboarding.mandatory: false` 退回原型 4.1 的可跳过行为。强制态必须使用 top layer 对话框,不得只依赖可能被 Host `transform/overflow` 限制的固定遮罩。点击账户菜单或个人态的“创建团队 / 企业”必须直接启动内部流程,不得向 Host 派发 `organization-create-requested` 或 `create-organization-requested`。真实 UniplatBase transport 复用 `createUniplatBaseOrganizationOnboardingTransport()` 与 `system.org/create_organization`。邀请码加入、企业认证、团队升级和邀请成员等未展开页面仍派发业务事件,由 Host 实现。
|
|
41
41
|
|
|
42
42
|
`<uniplat-organization-selector>` 覆盖 40.1 原型 S.1 多归属选择弹窗,显示 Host 提供的完整企业/团队列表、类型、认证状态、角色、成员数和最近进入信息,并自行负责脱敏账户摘要、名称与统一社会信用代码搜索、全部/企业/团队类型筛选、“最近进入 / 全部组织”分组、默认选中和带目标名称的主按钮。选中与记住偏好只在组件内临时维护,点击“进入”后交由 Host 完成登录态切换,“退出登录”只派发事件。`presentation: 'modal'` 时组件自渲染 S.1 强制态弹窗:固定遮罩、无关闭按钮,遮罩点击与 `Escape` 都不关闭,`Escape` 也不冒泡到 Host 的全局关闭逻辑;默认 `inline` 只输出卡片,遮罩归 Host。S.0 的“弹不弹”判定由纯函数 `resolveOrganizationEntryDecision()` 提供,只按 0 个归属、1 个归属、命中记住选择、2 个及以上分别返回 `personal | auto-enter | prompt`;它不读写宿主存储,记住偏好由 Host 自行持久化并回传,记住的组织已不在列表时退回 `prompt`。
|
|
43
43
|
|
|
@@ -324,10 +324,12 @@ userMenu.configure({
|
|
|
324
324
|
```ts
|
|
325
325
|
accountContext.configure({
|
|
326
326
|
applicationName,
|
|
327
|
-
organizationOnboarding: { transport },
|
|
327
|
+
organizationOnboarding: { transport, mandatory },
|
|
328
328
|
})
|
|
329
329
|
```
|
|
330
330
|
|
|
331
|
+
`mandatory` 默认 `true`,非布尔值一律拒绝。
|
|
332
|
+
|
|
331
333
|
transport 只提供 `createEnterprise()` 和 `createTeam()`;稳定冲突错误使用 `OrganizationOnboardingTransportError`,不得把原始响应、JWT 或内部异常透传给组件。`createEnterprise()` 接收组织名称、企业名称与统一社会信用代码,`createTeam()` 只接收团队名称。创建成功响应只包含稳定 ID、展示名称和 `enterprise | team` 类型。
|
|
332
334
|
|
|
333
335
|
真实 UniplatBase 对接使用 `createUniplatBaseOrganizationOnboardingTransport({ serviceBaseUrl, getAccessToken })`,复用 `system.org/create_organization`。每次创建前必须重新取 JWT,并只放入 `Authorization: Bearer` header;请求不得携带调用方身份字段。只对白名单冲突码做字段级映射,`INVALID_ARGUMENT`、`ORGANIZATION_CREATE_FAILED` 和所有非协议异常统一收敛为 `UNKNOWN`。
|
|
@@ -435,7 +437,7 @@ npm 包名、导出类型名、Custom Element 标签、`configure()` 结构、
|
|
|
435
437
|
- Account Context 内嵌工作台需测试不注册独立应用切换标签、JWT 只交给 transport、HTTP Authorization header、入口净化、当前组织级应用加 `application_scope=open` 应用的筛选、缺字段时 `oid=0` 兼容、`issue_ticket` 空请求体、签发响应来源进入新 Fragment、公共事件无 JWT/Ticket、Ticket 只进入局部导航 URL、目标页面先清 Fragment 再按初始化应用登录、响应应用绑定校验、加载错误、仅请求应用列表和应用选择交互。
|
|
436
438
|
- User Login 需测试组件注册与导出、三个固定页签、不可用能力禁用、原型中不存在图形验证码且不调用兼容方法、逐字段失焦校验和协议错误态、稳定后端错误码映射、短信倒计时、密码登录、扫码待确认/已扫码/过期/成功状态、注册成功过渡页、2 秒自动入驻和立即开始、页签切换与断开后的定时器清理、登录回调,以及 JWT、密码和二维码 challenge 不进入 URL、DOM、公共事件、日志或安全错误。
|
|
437
439
|
- User Menu 需测试组件注册与导出、40.1 6.7 原型关键内容、个人账号与当前组织员工信息入口、企业认证与团队样式、安全头像与文字回退、Host 受控选中状态、重新配置、空组织状态、完整手机号拒绝、创建与退出事件、普通文本插值,以及公共事件只包含必要的组织 ID,不包含显示名、手机号、组织名称、头像 URL、JWT 或内部配置。
|
|
438
|
-
- 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT
|
|
440
|
+
- 内置组织入驻需测试所有原型步骤、逐字段校验、稳定 transport 错误映射、企业/团队成功状态、跳过与未展开流程事件、普通文本插值,以及公共事件不包含企业信息、信用代码、JWT 或原始异常。强制入驻还需测试零归属投影自动打开 top layer 对话框、不渲染跳过与无归属提示、`Escape` 与外部点击都不关闭、“退出登录”只派发空 detail、表单页“返回”回到创建方式选择、成功页动作才关闭对话框,以及 `mandatory: false` 保持原可跳过行为。真实 UniplatBase transport 还必须测试每次请求重新取 JWT、JWT 只进入 Authorization header、企业/团队请求字段、成功响应净化和未知错误收敛。
|
|
439
441
|
- Organization Selector 需测试完整列表、企业认证与团队类型样式、脱敏账户摘要与完整手机号拒绝、按名称和统一社会信用代码搜索且代码不进入 DOM、类型筛选与常显计数、“最近进入 / 全部组织”分组与默认选中、带目标名称的主按钮、无匹配与空归属状态、记住偏好、创建/进入/退出登录事件、`modal` 强制态无关闭按钮且遮罩点击与 `Escape` 都不关闭并在移除后解除 `Escape` 拦截、非法 `presentation` 与重复 ID 拒绝和普通文本插值;事件只包含稳定组织 ID 与偏好,不含姓名、手机号、组织名称或信用代码。`resolveOrganizationEntryDecision()` 需测试 0 个归属、单一归属、命中与失效的记住选择、2 个及以上的默认选中项和非法输入拒绝。
|
|
440
442
|
- Organization Multi Employee Selector 需测试部门树、员工分页和搜索、初始选择补取、跨部门按员工 ID 去重、多选上限、受保护项、确认/取消事件、独立滚动、移动视图、错误重试、身份切换清理,以及 JWT、完整手机号、身份字段和后端错误不进入 DOM、事件、URL或日志。专用 transport 必须测试每次请求重新读取 JWT、只使用 Authorization header、组织级 directory scope 和无调用方身份参数。
|
|
441
443
|
- Organization Single Employee Selector 需测试独立标签与导出、初始单个 ID 回显、新选择替换旧选择、未选时禁止确认、受保护项、标量确认 detail、拒绝多选打开参数,以及与多选组件相同的数据和认证安全边界。
|
package/README.md
CHANGED
|
@@ -298,7 +298,22 @@ accountContext.configure({
|
|
|
298
298
|
|
|
299
299
|
该 transport 复用 `POST system.org/create_organization`。企业请求映射为 `organization_type/team_name/customer_name/credit_code`,团队请求只发送 `organization_type/team_name`;不会提交 `uid/oid/eid/owner` 等身份字段。它在每次创建前重新调用 `getAccessToken()`,JWT 只进入 `Authorization: Bearer` header,成功响应只保留组件需要的组织 ID、展示名称和类型。`INVALID_ARGUMENT`、`ORGANIZATION_CREATE_FAILED`、HTTP、解析及 token provider 异常均收敛为 `UNKNOWN`,不透传后端正文。
|
|
300
300
|
|
|
301
|
-
内部流程会经账户组件派发 `onboarding-skipped`、`join-organization-requested`、`organization-created`、`enterprise-authentication-requested`、`enterprise-authentication-deferred`、`team-upgrade-requested`、`team-invite-members-requested` 和安全的 `error` 事件。Host 可通过账户组件的 `onboarding-panel` part
|
|
301
|
+
内部流程会经账户组件派发 `onboarding-skipped`、`join-organization-requested`、`organization-created`、`enterprise-authentication-requested`、`enterprise-authentication-deferred`、`team-upgrade-requested`、`team-invite-members-requested`、`logout-requested` 和安全的 `error` 事件。Host 可通过账户组件的 `onboarding-panel` part 定制该浮层或对话框。
|
|
302
|
+
|
|
303
|
+
无归属用户默认必须先入驻。账户投影返回零归属时,组件用原生 `dialog.showModal()` 自动打开强制态创建流程:
|
|
304
|
+
|
|
305
|
+
```js
|
|
306
|
+
accountContext.configure({
|
|
307
|
+
applicationName: 'OfficialWebsite',
|
|
308
|
+
transport: accountTransport,
|
|
309
|
+
organizationOnboarding: {
|
|
310
|
+
transport: organizationOnboardingTransport,
|
|
311
|
+
mandatory: true, // 默认值;传 false 回到可跳过的原型 4.1 行为
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
强制态下不渲染“跳过”“暂不创建,先逛逛”“用邀请码加入”和关闭按钮,遮罩点击、`Escape` 与原生 `cancel` 都不关闭,用户只有两条出路:创建组织,或点“退出登录”(派发 `logout-requested`,由 Host 清理登录态)。企业/团队表单页提供“返回”回到创建方式选择,避免困在错误分支;创建成功页的“去认证 / 稍后认证 / 升级为企业 / 邀请成员”是关闭对话框的终点。对话框使用 top layer,不受 Host 导航 `transform` 或 `overflow` 影响。
|
|
302
317
|
|
|
303
318
|
### Organization Selector
|
|
304
319
|
|
package/package.json
CHANGED
|
@@ -117,6 +117,11 @@ export interface AccountContextConfig {
|
|
|
117
117
|
*/
|
|
118
118
|
organizationOnboarding?: {
|
|
119
119
|
transport: OrganizationOnboardingTransport
|
|
120
|
+
/**
|
|
121
|
+
* 无归属用户是否必须先入驻。默认开启:加载到零归属投影时组件自动以
|
|
122
|
+
* 强制模式对话框打开创建流程,用户只能创建组织或退出登录。
|
|
123
|
+
*/
|
|
124
|
+
mandatory?: boolean
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
|
|
@@ -172,6 +177,7 @@ export class UniplatAccountContext extends LitElement {
|
|
|
172
177
|
viewModel: { state: true },
|
|
173
178
|
activePanel: { state: true },
|
|
174
179
|
employeeInfo: { state: true },
|
|
180
|
+
mandatoryOnboarding: { state: true },
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
static override styles = css`
|
|
@@ -450,6 +456,35 @@ export class UniplatAccountContext extends LitElement {
|
|
|
450
456
|
width: min(400px, calc(100vw - 24px));
|
|
451
457
|
}
|
|
452
458
|
|
|
459
|
+
/*
|
|
460
|
+
* 强制入驻使用原生 dialog 的 top layer,不受 Host 导航 transform / overflow 影响;
|
|
461
|
+
* 无关闭按钮,遮罩点击与 Escape 都不关闭。
|
|
462
|
+
*/
|
|
463
|
+
.onboarding-modal {
|
|
464
|
+
position: fixed;
|
|
465
|
+
z-index: 1200;
|
|
466
|
+
inset: 0;
|
|
467
|
+
display: grid;
|
|
468
|
+
width: 100vw;
|
|
469
|
+
max-width: none;
|
|
470
|
+
height: 100dvh;
|
|
471
|
+
max-height: none;
|
|
472
|
+
overflow: auto;
|
|
473
|
+
margin: 0;
|
|
474
|
+
padding: 24px;
|
|
475
|
+
border: 0;
|
|
476
|
+
background: transparent;
|
|
477
|
+
place-items: center;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.onboarding-modal::backdrop {
|
|
481
|
+
background: rgb(20 35 56 / 56%);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.onboarding-modal uniplat-internal-organization-onboarding {
|
|
485
|
+
width: min(400px, 100%);
|
|
486
|
+
}
|
|
487
|
+
|
|
453
488
|
.employee-info-modal {
|
|
454
489
|
position: fixed;
|
|
455
490
|
z-index: 1100;
|
|
@@ -680,9 +715,12 @@ export class UniplatAccountContext extends LitElement {
|
|
|
680
715
|
declare private viewModel: ResolvedAccountContext | null
|
|
681
716
|
declare private activePanel: ActivePanel
|
|
682
717
|
declare private employeeInfo: AccountContextEmployeeInfo | null
|
|
718
|
+
declare private mandatoryOnboarding: boolean
|
|
683
719
|
|
|
684
720
|
private workbenchConfig: ApplicationSwitcherConfig | null = null
|
|
685
721
|
private organizationOnboardingTransport: OrganizationOnboardingTransport | null = null
|
|
722
|
+
private organizationOnboardingMandatory = true
|
|
723
|
+
private configuredOnboardingMandatory = false
|
|
686
724
|
private applicationName = ''
|
|
687
725
|
private accountTransport: AccountContextTransport | null = null
|
|
688
726
|
private showWorkbench = true
|
|
@@ -699,6 +737,7 @@ export class UniplatAccountContext extends LitElement {
|
|
|
699
737
|
this.viewModel = null
|
|
700
738
|
this.activePanel = null
|
|
701
739
|
this.employeeInfo = null
|
|
740
|
+
this.mandatoryOnboarding = false
|
|
702
741
|
}
|
|
703
742
|
|
|
704
743
|
configure(config: AccountContextConfig): void {
|
|
@@ -711,6 +750,10 @@ export class UniplatAccountContext extends LitElement {
|
|
|
711
750
|
this.organizationOnboardingTransport = normalizeOrganizationOnboardingTransport(
|
|
712
751
|
config.organizationOnboarding?.transport,
|
|
713
752
|
)
|
|
753
|
+
this.organizationOnboardingMandatory = normalizeMandatoryOnboarding(
|
|
754
|
+
config.organizationOnboarding?.mandatory,
|
|
755
|
+
)
|
|
756
|
+
this.mandatoryOnboarding = false
|
|
714
757
|
this.configuredApplicationSwitcher = null
|
|
715
758
|
this.configuredUserMenu = null
|
|
716
759
|
this.configuredOrganizationOnboarding = null
|
|
@@ -837,6 +880,29 @@ export class UniplatAccountContext extends LitElement {
|
|
|
837
880
|
showWorkbench: config.showWorkbench ?? true,
|
|
838
881
|
showVerificationStatus: organization ? (config.showVerificationStatus ?? true) : false,
|
|
839
882
|
}
|
|
883
|
+
this.syncMandatoryOnboarding()
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* 无归属用户必须先入驻:加载到零归属投影时自动以强制模式对话框打开创建流程。
|
|
888
|
+
* 创建成功后投影不再为空,但对话框保持强制态直到用户在成功页完成后续动作,
|
|
889
|
+
* 避免界面从模式对话框跳回小浮层。
|
|
890
|
+
*/
|
|
891
|
+
private syncMandatoryOnboarding(): void {
|
|
892
|
+
if (
|
|
893
|
+
this.organizationOnboardingMandatory &&
|
|
894
|
+
this.organizationOnboardingTransport &&
|
|
895
|
+
this.viewModel &&
|
|
896
|
+
this.viewModel.organizations.length === 0 &&
|
|
897
|
+
!this.viewModel.organization
|
|
898
|
+
) {
|
|
899
|
+
this.mandatoryOnboarding = true
|
|
900
|
+
this.activePanel = 'onboarding'
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private get onboardingLocked(): boolean {
|
|
905
|
+
return this.mandatoryOnboarding && this.activePanel === 'onboarding'
|
|
840
906
|
}
|
|
841
907
|
|
|
842
908
|
override connectedCallback(): void {
|
|
@@ -976,14 +1042,26 @@ export class UniplatAccountContext extends LitElement {
|
|
|
976
1042
|
const onboarding = this.renderRoot.querySelector<InternalOrganizationOnboarding>(
|
|
977
1043
|
INTERNAL_ORGANIZATION_ONBOARDING_TAG,
|
|
978
1044
|
)
|
|
979
|
-
if (
|
|
1045
|
+
if (
|
|
1046
|
+
onboarding &&
|
|
1047
|
+
(onboarding !== this.configuredOrganizationOnboarding ||
|
|
1048
|
+
this.configuredOnboardingMandatory !== this.mandatoryOnboarding)
|
|
1049
|
+
) {
|
|
980
1050
|
const transport = this.organizationOnboardingTransport
|
|
981
1051
|
if (transport) {
|
|
982
|
-
onboarding.configure({
|
|
1052
|
+
onboarding.configure({
|
|
1053
|
+
transport,
|
|
1054
|
+
initialStep: 'method-selection',
|
|
1055
|
+
mandatory: this.mandatoryOnboarding,
|
|
1056
|
+
})
|
|
983
1057
|
this.configuredOrganizationOnboarding = onboarding
|
|
1058
|
+
this.configuredOnboardingMandatory = this.mandatoryOnboarding
|
|
984
1059
|
}
|
|
985
1060
|
}
|
|
986
|
-
if (!onboarding)
|
|
1061
|
+
if (!onboarding) {
|
|
1062
|
+
this.configuredOrganizationOnboarding = null
|
|
1063
|
+
this.configuredOnboardingMandatory = false
|
|
1064
|
+
}
|
|
987
1065
|
|
|
988
1066
|
const employeeInfoModal = this.renderRoot.querySelector<HTMLDialogElement>(
|
|
989
1067
|
'dialog.employee-info-modal',
|
|
@@ -995,6 +1073,16 @@ export class UniplatAccountContext extends LitElement {
|
|
|
995
1073
|
employeeInfoModal.setAttribute('open', '')
|
|
996
1074
|
}
|
|
997
1075
|
}
|
|
1076
|
+
|
|
1077
|
+
const onboardingModal =
|
|
1078
|
+
this.renderRoot.querySelector<HTMLDialogElement>('dialog.onboarding-modal')
|
|
1079
|
+
if (onboardingModal && !onboardingModal.open) {
|
|
1080
|
+
try {
|
|
1081
|
+
onboardingModal.showModal()
|
|
1082
|
+
} catch {
|
|
1083
|
+
onboardingModal.setAttribute('open', '')
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
998
1086
|
}
|
|
999
1087
|
|
|
1000
1088
|
private renderOrganizationLine(
|
|
@@ -1037,13 +1125,30 @@ export class UniplatAccountContext extends LitElement {
|
|
|
1037
1125
|
</div>`
|
|
1038
1126
|
}
|
|
1039
1127
|
if (this.activePanel === 'onboarding' && this.organizationOnboardingTransport) {
|
|
1128
|
+
const onboarding = html`<uniplat-internal-organization-onboarding></uniplat-internal-organization-onboarding>`
|
|
1129
|
+
if (this.mandatoryOnboarding) {
|
|
1130
|
+
return html`<dialog
|
|
1131
|
+
class="onboarding-modal"
|
|
1132
|
+
part="onboarding-panel"
|
|
1133
|
+
aria-label="创建团队或企业"
|
|
1134
|
+
@cancel=${this.blockMandatoryOnboardingCancel}
|
|
1135
|
+
@onboarding-skipped=${this.handleOnboardingSkipped}
|
|
1136
|
+
@organization-created=${this.refreshAfterOrganizationCreated}
|
|
1137
|
+
@enterprise-authentication-requested=${this.finishMandatoryOnboarding}
|
|
1138
|
+
@enterprise-authentication-deferred=${this.finishMandatoryOnboarding}
|
|
1139
|
+
@team-upgrade-requested=${this.finishMandatoryOnboarding}
|
|
1140
|
+
@team-invite-members-requested=${this.finishMandatoryOnboarding}
|
|
1141
|
+
>
|
|
1142
|
+
${onboarding}
|
|
1143
|
+
</dialog>`
|
|
1144
|
+
}
|
|
1040
1145
|
return html`<div
|
|
1041
1146
|
class="floating-panel onboarding-panel"
|
|
1042
1147
|
part="onboarding-panel"
|
|
1043
|
-
@onboarding-skipped=${this.
|
|
1148
|
+
@onboarding-skipped=${this.handleOnboardingSkipped}
|
|
1044
1149
|
@organization-created=${this.refreshAfterOrganizationCreated}
|
|
1045
1150
|
>
|
|
1046
|
-
|
|
1151
|
+
${onboarding}
|
|
1047
1152
|
</div>`
|
|
1048
1153
|
}
|
|
1049
1154
|
if (this.activePanel === 'account') {
|
|
@@ -1194,6 +1299,24 @@ export class UniplatAccountContext extends LitElement {
|
|
|
1194
1299
|
this.activePanel = null
|
|
1195
1300
|
}
|
|
1196
1301
|
|
|
1302
|
+
private readonly blockMandatoryOnboardingCancel = (event: Event): void => {
|
|
1303
|
+
// 强制入驻不允许 Escape 关闭原生对话框。
|
|
1304
|
+
if (this.onboardingLocked) event.preventDefault()
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
private readonly handleOnboardingSkipped = (): void => {
|
|
1308
|
+
// 强制入驻没有跳过入口;即使宿主自行派发也不放行。
|
|
1309
|
+
if (this.onboardingLocked) return
|
|
1310
|
+
this.activePanel = null
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 成功页的认证 / 升级 / 邀请动作是强制入驻的终点,此时才允许关闭对话框。
|
|
1314
|
+
private readonly finishMandatoryOnboarding = (): void => {
|
|
1315
|
+
if (!this.mandatoryOnboarding) return
|
|
1316
|
+
this.mandatoryOnboarding = false
|
|
1317
|
+
this.activePanel = null
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1197
1320
|
private readonly openEmployeeInfo = (): void => {
|
|
1198
1321
|
if (!this.viewModel?.organization) {
|
|
1199
1322
|
this.activePanel = null
|
|
@@ -1268,11 +1391,17 @@ export class UniplatAccountContext extends LitElement {
|
|
|
1268
1391
|
}
|
|
1269
1392
|
|
|
1270
1393
|
private readonly handleDocumentPointerDown = (event: PointerEvent): void => {
|
|
1394
|
+
if (this.onboardingLocked) return
|
|
1271
1395
|
if (this.activePanel && !event.composedPath().includes(this)) this.activePanel = null
|
|
1272
1396
|
}
|
|
1273
1397
|
|
|
1274
1398
|
private readonly handleDocumentKeyDown = (event: KeyboardEvent): void => {
|
|
1275
1399
|
if (event.key !== 'Escape' || !this.activePanel) return
|
|
1400
|
+
if (this.onboardingLocked) {
|
|
1401
|
+
event.preventDefault()
|
|
1402
|
+
event.stopPropagation()
|
|
1403
|
+
return
|
|
1404
|
+
}
|
|
1276
1405
|
const activePanel = this.activePanel
|
|
1277
1406
|
this.activePanel = null
|
|
1278
1407
|
const selector = activePanel === 'workbench' ? '.workbench' : '.account'
|
|
@@ -1333,6 +1462,14 @@ function normalizeWorkbench(
|
|
|
1333
1462
|
return workbench
|
|
1334
1463
|
}
|
|
1335
1464
|
|
|
1465
|
+
function normalizeMandatoryOnboarding(value: unknown): boolean {
|
|
1466
|
+
if (value === undefined) return true
|
|
1467
|
+
if (typeof value !== 'boolean') {
|
|
1468
|
+
throw new TypeError('account-context organizationOnboarding.mandatory must be a boolean')
|
|
1469
|
+
}
|
|
1470
|
+
return value
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1336
1473
|
function normalizeOrganizationOnboardingTransport(
|
|
1337
1474
|
transport: OrganizationOnboardingTransport | undefined,
|
|
1338
1475
|
): OrganizationOnboardingTransport | null {
|
|
@@ -17,12 +17,17 @@ export type OrganizationOnboardingStep =
|
|
|
17
17
|
export interface OrganizationOnboardingConfig {
|
|
18
18
|
transport: OrganizationOnboardingTransport
|
|
19
19
|
initialStep?: 'unassigned' | 'method-selection'
|
|
20
|
+
/**
|
|
21
|
+
* 强制入驻:隐藏所有跳过入口,无归属用户只能创建组织或退出登录。
|
|
22
|
+
*/
|
|
23
|
+
mandatory?: boolean
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export interface OrganizationOnboardingEventMap {
|
|
23
27
|
'onboarding-skipped': CustomEvent<{
|
|
24
28
|
source: 'unassigned' | 'method-selection' | 'enterprise-form' | 'team-form'
|
|
25
29
|
}>
|
|
30
|
+
'logout-requested': CustomEvent<Record<string, never>>
|
|
26
31
|
'join-organization-requested': CustomEvent<{ enterpriseName?: string }>
|
|
27
32
|
'organization-created': CustomEvent<{ organizationId: string; type: 'enterprise' | 'team' }>
|
|
28
33
|
'enterprise-authentication-requested': CustomEvent<{ organizationId: string }>
|
|
@@ -468,6 +473,7 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
468
473
|
|
|
469
474
|
private transport: OrganizationOnboardingTransport | null = null
|
|
470
475
|
private skipReturnStep: SkippableStep = 'method-selection'
|
|
476
|
+
private mandatory = false
|
|
471
477
|
|
|
472
478
|
constructor() {
|
|
473
479
|
super()
|
|
@@ -494,13 +500,18 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
494
500
|
if (config.initialStep && !['unassigned', 'method-selection'].includes(config.initialStep)) {
|
|
495
501
|
throw new TypeError('organization-onboarding initialStep is invalid')
|
|
496
502
|
}
|
|
503
|
+
if (config.mandatory !== undefined && typeof config.mandatory !== 'boolean') {
|
|
504
|
+
throw new TypeError('organization-onboarding mandatory must be a boolean')
|
|
505
|
+
}
|
|
497
506
|
this.transport = config.transport
|
|
498
|
-
this.
|
|
507
|
+
this.mandatory = config.mandatory ?? false
|
|
508
|
+
// 强制入驻不展示带“暂不创建”的无归属提示,直接进入创建流程。
|
|
509
|
+
this.reset(this.mandatory ? 'method-selection' : (config.initialStep ?? 'method-selection'))
|
|
499
510
|
}
|
|
500
511
|
|
|
501
512
|
start(step: 'unassigned' | 'method-selection' = 'method-selection'): void {
|
|
502
513
|
if (!this.transport) throw new Error('organization-onboarding is not configured')
|
|
503
|
-
this.reset(step)
|
|
514
|
+
this.reset(this.mandatory ? 'method-selection' : step)
|
|
504
515
|
}
|
|
505
516
|
|
|
506
517
|
override render() {
|
|
@@ -513,6 +524,41 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
513
524
|
return this.renderSuccess()
|
|
514
525
|
}
|
|
515
526
|
|
|
527
|
+
/**
|
|
528
|
+
* 普通模式保留原型的“跳过”;强制入驻时创建流程首屏只提供“退出登录”,
|
|
529
|
+
* 表单页提供“返回”回到创建方式选择,用户不会被困在某个分支里。
|
|
530
|
+
*/
|
|
531
|
+
private renderHeadingAction(step: SkippableStep) {
|
|
532
|
+
if (!this.mandatory) {
|
|
533
|
+
return html`<button
|
|
534
|
+
class="skip"
|
|
535
|
+
part="skip-action"
|
|
536
|
+
type="button"
|
|
537
|
+
@click=${() => this.openSkip(step)}
|
|
538
|
+
>
|
|
539
|
+
跳过
|
|
540
|
+
</button>`
|
|
541
|
+
}
|
|
542
|
+
if (step === 'method-selection') {
|
|
543
|
+
return html`<button
|
|
544
|
+
class="skip"
|
|
545
|
+
part="logout-action"
|
|
546
|
+
type="button"
|
|
547
|
+
@click=${this.requestLogout}
|
|
548
|
+
>
|
|
549
|
+
退出登录
|
|
550
|
+
</button>`
|
|
551
|
+
}
|
|
552
|
+
return html`<button
|
|
553
|
+
class="skip"
|
|
554
|
+
part="back-action"
|
|
555
|
+
type="button"
|
|
556
|
+
@click=${this.openMethodSelection}
|
|
557
|
+
>
|
|
558
|
+
返回
|
|
559
|
+
</button>`
|
|
560
|
+
}
|
|
561
|
+
|
|
516
562
|
private renderUnassigned() {
|
|
517
563
|
return html`
|
|
518
564
|
<section class="card" part="shell" aria-label="无归属团队或企业">
|
|
@@ -565,14 +611,7 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
565
611
|
办理社保、薪酬、用工等业务需先归属一个团队 / 企业
|
|
566
612
|
</p>
|
|
567
613
|
</div>
|
|
568
|
-
|
|
569
|
-
class="skip"
|
|
570
|
-
part="skip-action"
|
|
571
|
-
type="button"
|
|
572
|
-
@click=${() => this.openSkip('method-selection')}
|
|
573
|
-
>
|
|
574
|
-
跳过
|
|
575
|
-
</button>
|
|
614
|
+
${this.renderHeadingAction('method-selection')}
|
|
576
615
|
</div>
|
|
577
616
|
<div class="options" role="list" part="method-options">
|
|
578
617
|
${this.renderOption(
|
|
@@ -672,14 +711,7 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
672
711
|
<div class="content">
|
|
673
712
|
<div class="heading-row">
|
|
674
713
|
<div><h2 part="title">填写企业信息</h2></div>
|
|
675
|
-
|
|
676
|
-
class="skip"
|
|
677
|
-
part="skip-action"
|
|
678
|
-
type="button"
|
|
679
|
-
@click=${() => this.openSkip('enterprise-form')}
|
|
680
|
-
>
|
|
681
|
-
跳过
|
|
682
|
-
</button>
|
|
714
|
+
${this.renderHeadingAction('enterprise-form')}
|
|
683
715
|
</div>
|
|
684
716
|
${
|
|
685
717
|
errorCount > 0
|
|
@@ -813,14 +845,7 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
813
845
|
<div class="content">
|
|
814
846
|
<div class="heading-row">
|
|
815
847
|
<div><h2 part="title">填写团队信息</h2></div>
|
|
816
|
-
|
|
817
|
-
class="skip"
|
|
818
|
-
part="skip-action"
|
|
819
|
-
type="button"
|
|
820
|
-
@click=${() => this.openSkip('team-form')}
|
|
821
|
-
>
|
|
822
|
-
跳过
|
|
823
|
-
</button>
|
|
848
|
+
${this.renderHeadingAction('team-form')}
|
|
824
849
|
</div>
|
|
825
850
|
<div class="team-notice" part="team-notice">
|
|
826
851
|
${icon('check')}<span
|
|
@@ -960,10 +985,15 @@ export class InternalOrganizationOnboarding extends LitElement {
|
|
|
960
985
|
}
|
|
961
986
|
|
|
962
987
|
private openSkip(returnStep: SkippableStep) {
|
|
988
|
+
if (this.mandatory) return
|
|
963
989
|
this.skipReturnStep = returnStep
|
|
964
990
|
this.step = 'skip-confirmation'
|
|
965
991
|
}
|
|
966
992
|
|
|
993
|
+
private readonly requestLogout = () => {
|
|
994
|
+
this.dispatchPublicEvent('logout-requested', {})
|
|
995
|
+
}
|
|
996
|
+
|
|
967
997
|
private readonly continueCreating = () => {
|
|
968
998
|
this.step = this.skipReturnStep
|
|
969
999
|
}
|
|
@@ -819,7 +819,7 @@ function normalizeOrganizations(
|
|
|
819
819
|
role: normalizeOptionalText(organization.role, 60),
|
|
820
820
|
memberCount: normalizeMemberCount(organization.memberCount),
|
|
821
821
|
lastEnteredLabel: normalizeOptionalText(organization.lastEnteredLabel, 80),
|
|
822
|
-
searchKey: `${name}
|
|
822
|
+
searchKey: `${name} ${unifiedSocialCreditCode}`.toLowerCase(),
|
|
823
823
|
}
|
|
824
824
|
})
|
|
825
825
|
}
|
|
@@ -59,6 +59,36 @@ async function settle(context: UniplatAccountContext): Promise<void> {
|
|
|
59
59
|
await Promise.resolve()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function emptyProjection(): AccountContextProjection {
|
|
63
|
+
return {
|
|
64
|
+
applicationName: APPLICATION_NAME,
|
|
65
|
+
user: { displayName: '131****2222', maskedMobile: '131****2222', avatarText: '1' },
|
|
66
|
+
currentOrganizationId: null,
|
|
67
|
+
currentOrganization: null,
|
|
68
|
+
organizations: [],
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function mountUnassigned(
|
|
73
|
+
mandatory?: boolean,
|
|
74
|
+
): Promise<{ context: UniplatAccountContext; logout: ReturnType<typeof vi.fn> }> {
|
|
75
|
+
const context = document.createElement(ACCOUNT_CONTEXT_TAG)
|
|
76
|
+
const logout = vi.fn()
|
|
77
|
+
context.addEventListener('logout-requested', logout)
|
|
78
|
+
context.configure({
|
|
79
|
+
applicationName: APPLICATION_NAME,
|
|
80
|
+
transport: { load: async () => emptyProjection() },
|
|
81
|
+
organizationOnboarding: {
|
|
82
|
+
transport: createMockOrganizationOnboardingTransport(),
|
|
83
|
+
...(mandatory === undefined ? {} : { mandatory }),
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
document.body.append(context)
|
|
87
|
+
await vi.waitFor(() => expect(context.shadowRoot?.querySelector('.display-name')).not.toBeNull())
|
|
88
|
+
await settle(context)
|
|
89
|
+
return { context, logout }
|
|
90
|
+
}
|
|
91
|
+
|
|
62
92
|
describe('<uniplat-account-context>', () => {
|
|
63
93
|
afterEach(() => {
|
|
64
94
|
document.body.replaceChildren()
|
|
@@ -193,6 +223,93 @@ describe('<uniplat-account-context>', () => {
|
|
|
193
223
|
expect(menu?.shadowRoot?.textContent).toContain('新创建团队')
|
|
194
224
|
})
|
|
195
225
|
|
|
226
|
+
it('forces unassigned users into a mandatory onboarding modal with only create or logout', async () => {
|
|
227
|
+
const { context, logout } = await mountUnassigned()
|
|
228
|
+
|
|
229
|
+
const modal = context.shadowRoot?.querySelector<HTMLDialogElement>('dialog.onboarding-modal')
|
|
230
|
+
expect(modal).not.toBeNull()
|
|
231
|
+
expect(modal?.getAttribute('aria-label')).toBe('创建团队或企业')
|
|
232
|
+
expect(modal?.hasAttribute('open')).toBe(true)
|
|
233
|
+
expect(context.shadowRoot?.querySelector('.floating-panel')).toBeNull()
|
|
234
|
+
|
|
235
|
+
const onboarding = context.shadowRoot?.querySelector('uniplat-internal-organization-onboarding')
|
|
236
|
+
const onboardingRoot = onboarding?.shadowRoot
|
|
237
|
+
expect(onboardingRoot?.textContent).toContain('创建你的团队 / 企业')
|
|
238
|
+
expect(onboardingRoot?.textContent).not.toContain('跳过')
|
|
239
|
+
expect(onboardingRoot?.textContent).not.toContain('暂不创建')
|
|
240
|
+
expect(onboardingRoot?.querySelector('[part="skip-action"]')).toBeNull()
|
|
241
|
+
|
|
242
|
+
const logoutAction = onboardingRoot?.querySelector<HTMLButtonElement>('[part="logout-action"]')
|
|
243
|
+
expect(logoutAction?.textContent?.trim()).toBe('退出登录')
|
|
244
|
+
logoutAction?.click()
|
|
245
|
+
expect(logout).toHaveBeenCalledTimes(1)
|
|
246
|
+
expect(logout.mock.calls[0]?.[0].detail).toEqual({})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('keeps the mandatory onboarding modal open on outside clicks and Escape', async () => {
|
|
250
|
+
const { context } = await mountUnassigned()
|
|
251
|
+
|
|
252
|
+
document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
|
253
|
+
await settle(context)
|
|
254
|
+
expect(context.shadowRoot?.querySelector('.onboarding-modal')).not.toBeNull()
|
|
255
|
+
|
|
256
|
+
const escape = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
257
|
+
document.dispatchEvent(escape)
|
|
258
|
+
await settle(context)
|
|
259
|
+
expect(escape.defaultPrevented).toBe(true)
|
|
260
|
+
expect(context.shadowRoot?.querySelector('.onboarding-modal')).not.toBeNull()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('offers a way back to the creation choice instead of skipping inside the forced flow', async () => {
|
|
264
|
+
const { context } = await mountUnassigned()
|
|
265
|
+
const onboarding = context.shadowRoot?.querySelector('uniplat-internal-organization-onboarding')
|
|
266
|
+
const onboardingRoot = onboarding?.shadowRoot
|
|
267
|
+
|
|
268
|
+
onboardingRoot?.querySelector<HTMLButtonElement>('[part="primary-action"]')?.click()
|
|
269
|
+
await onboarding?.updateComplete
|
|
270
|
+
expect(onboardingRoot?.textContent).toContain('填写企业信息')
|
|
271
|
+
const back = onboardingRoot?.querySelector<HTMLButtonElement>('[part="back-action"]')
|
|
272
|
+
expect(back?.textContent?.trim()).toBe('返回')
|
|
273
|
+
expect(onboardingRoot?.querySelector('[part="skip-action"]')).toBeNull()
|
|
274
|
+
|
|
275
|
+
back?.click()
|
|
276
|
+
await onboarding?.updateComplete
|
|
277
|
+
expect(onboardingRoot?.textContent).toContain('创建你的团队 / 企业')
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('closes the mandatory modal only after the created organization success action', async () => {
|
|
281
|
+
const { context } = await mountUnassigned()
|
|
282
|
+
const onboarding = context.shadowRoot?.querySelector('uniplat-internal-organization-onboarding')
|
|
283
|
+
const onboardingRoot = onboarding?.shadowRoot
|
|
284
|
+
|
|
285
|
+
onboardingRoot?.querySelector<HTMLButtonElement>('[part="primary-action"]')?.click()
|
|
286
|
+
await onboarding?.updateComplete
|
|
287
|
+
const inputs = [...(onboardingRoot?.querySelectorAll<HTMLInputElement>('[part="input"]') ?? [])]
|
|
288
|
+
const values = ['星河科技', '北京星河科技有限公司', '91110108MA01XY7788']
|
|
289
|
+
inputs.forEach((input, index) => {
|
|
290
|
+
input.value = values[index] ?? ''
|
|
291
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
292
|
+
})
|
|
293
|
+
await onboarding?.updateComplete
|
|
294
|
+
onboardingRoot?.querySelector('form')?.dispatchEvent(new Event('submit', { bubbles: true }))
|
|
295
|
+
|
|
296
|
+
await vi.waitFor(() => expect(onboardingRoot?.textContent).toContain('已创建'))
|
|
297
|
+
expect(context.shadowRoot?.querySelector('.onboarding-modal')).not.toBeNull()
|
|
298
|
+
|
|
299
|
+
onboardingRoot?.querySelector<HTMLButtonElement>('[part="secondary-action"]')?.click()
|
|
300
|
+
await settle(context)
|
|
301
|
+
expect(context.shadowRoot?.querySelector('.onboarding-modal')).toBeNull()
|
|
302
|
+
expect(context.shadowRoot?.querySelector('[part="onboarding-panel"]')).toBeNull()
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
it('keeps the previous optional onboarding behaviour when mandatory is disabled', async () => {
|
|
306
|
+
const { context } = await mountUnassigned(false)
|
|
307
|
+
|
|
308
|
+
expect(context.shadowRoot?.querySelector('.onboarding-modal')).toBeNull()
|
|
309
|
+
expect(context.shadowRoot?.querySelector('[part="onboarding-panel"]')).toBeNull()
|
|
310
|
+
expect(context.shadowRoot?.textContent).toContain('创建团队 / 企业')
|
|
311
|
+
})
|
|
312
|
+
|
|
196
313
|
it('registers and renders the personal account navigation state', async () => {
|
|
197
314
|
expect(customElements.get(ACCOUNT_CONTEXT_TAG)).toBe(UniplatAccountContext)
|
|
198
315
|
const context = document.createElement(ACCOUNT_CONTEXT_TAG)
|
|
@@ -143,6 +143,10 @@ describe('<uniplat-organization-selector>', () => {
|
|
|
143
143
|
expect(rowNames(selector)).toEqual(['北京星河科技有限公司'])
|
|
144
144
|
expect(selector.shadowRoot?.textContent).not.toContain('91110108MA01XY7788')
|
|
145
145
|
|
|
146
|
+
// 名称与信用代码之间用空格分隔,搜索键里不得出现控制字符。
|
|
147
|
+
await typeSearch(selector, '北京星河科技有限公司 91110108MA01XY7788')
|
|
148
|
+
expect(rowNames(selector)).toEqual(['北京星河科技有限公司'])
|
|
149
|
+
|
|
146
150
|
await typeSearch(selector, '不存在的组织')
|
|
147
151
|
expect(rowNames(selector)).toEqual([])
|
|
148
152
|
expect(selector.shadowRoot?.textContent).toContain('没有匹配的团队或企业')
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
>
|
|
7
7
|
> 分支:`main`
|
|
8
8
|
>
|
|
9
|
-
> 交接基线提交:`
|
|
9
|
+
> 交接基线提交:`d19ac72 feat(web-components): 无归属用户强制走创建组织流程`
|
|
10
10
|
|
|
11
11
|
## 1. Claude 接手后的第一步
|
|
12
12
|
|
|
@@ -23,9 +23,9 @@ git log -5 --oneline --decorate
|
|
|
23
23
|
交接文档更新前的 Git 状态为:
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
main 领先 origin/main
|
|
26
|
+
main 领先 origin/main 9 个提交(未推送)
|
|
27
27
|
工作树无代码改动
|
|
28
|
-
HEAD =
|
|
28
|
+
HEAD = d19ac72
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
不要清理、覆盖或提交根目录已有的 `.tgz`。它们是历史及当前本地发布产物,已由 `.gitignore` 忽略。
|
|
@@ -38,9 +38,9 @@ HEAD = 7d2f9bb
|
|
|
38
38
|
|
|
39
39
|
| 位置 | npm 名称 | 当前本地版本 | 说明 |
|
|
40
40
|
| ------------------------- | -------------------------------------- | -----------: | --------------------------------------- |
|
|
41
|
-
| 根目录 | `@csntgao/uni-base` | `0.
|
|
41
|
+
| 根目录 | `@csntgao/uni-base` | `0.2.0` | 当前公开发布入口;npmjs 已发布此版本 |
|
|
42
42
|
| `packages/core` | `@uniplat/ai-employee-client` | `0.14.0` | UI 无关的 HTTP/WebSocket/Core 状态机 |
|
|
43
|
-
| `packages/web-components` | `@uniplat/ai-employee-web-components` | `0.
|
|
43
|
+
| `packages/web-components` | `@uniplat/ai-employee-web-components` | `0.25.0` | Lit Web Components 与独立浏览器 runtime |
|
|
44
44
|
| `examples/vanilla` | `@uniplat/ai-employee-vanilla-example` | 私有示例 | 必须消费 workspace 包的构建产物 |
|
|
45
45
|
|
|
46
46
|
注意:根包改名为 `@csntgao/uni-base` 不代表两个 workspace 包、已有 Web Component 标签或公开类型可以随之改名。`@uniplat/*` 名称和 `uniplat-*` 标签仍是公开契约。
|
|
@@ -71,6 +71,36 @@ HEAD = 7d2f9bb
|
|
|
71
71
|
|
|
72
72
|
## 4. 近期完成的改动
|
|
73
73
|
|
|
74
|
+
### 4.-2 无归属用户强制入驻
|
|
75
|
+
|
|
76
|
+
最新提交:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
d19ac72 feat(web-components): 无归属用户强制走创建组织流程
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
账户投影零归属时,`<uniplat-account-context>` 用原生 `dialog.showModal()`(top layer,不受 Host
|
|
83
|
+
导航 `transform/overflow` 影响)自动打开强制态创建流程:不渲染跳过、无归属提示、邀请码加入与
|
|
84
|
+
关闭按钮,遮罩点击、`Escape` 与 `cancel` 都不关闭,只能创建组织或点“退出登录”派发
|
|
85
|
+
`logout-requested`。企业/团队表单页的“跳过”换成“返回”,创建成功页的认证、升级或邀请动作才关闭
|
|
86
|
+
对话框。Host 可用 `organizationOnboarding.mandatory: false` 退回可跳过行为,默认开启。
|
|
87
|
+
|
|
88
|
+
该行为**覆盖**了 40.1 原型 4.1 的“暂不创建,先逛逛”和 S.0 图中“0 归属不弹窗”两条既有约定,
|
|
89
|
+
是按用户新需求做的行为变更,不是实现偏差。
|
|
90
|
+
|
|
91
|
+
### 4.-1 账户胶囊固定宽度
|
|
92
|
+
|
|
93
|
+
最新提交:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
605c783 fix(web-components): 固定账户胶囊宽度并截断过长姓名与组织名
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
导航右侧账户胶囊的 `.copy` 固定 96px;胶囊内姓名超过 6 个字、当前组织名称超过 8 个字按
|
|
100
|
+
字符数截断为 `…`,完整值只写入 `title`,未超长时不加 `title`。展开后的 User Menu 仍显示
|
|
101
|
+
完整名称,截断只作用于胶囊。相关常量在 `packages/web-components/src/account-context.ts`
|
|
102
|
+
的 `CAPSULE_DISPLAY_NAME_MAX_CHARS` 与 `CAPSULE_ORGANIZATION_NAME_MAX_CHARS`。
|
|
103
|
+
|
|
74
104
|
### 4.0 多归属组织选择改为 S.0 / S.1
|
|
75
105
|
|
|
76
106
|
最新提交:
|
|
@@ -244,13 +274,13 @@ Account Context 对外只派发净化后的 `LOAD_FAILED`,精确排错应看
|
|
|
244
274
|
|
|
245
275
|
## 7. 当前构建和验证状态
|
|
246
276
|
|
|
247
|
-
在 `
|
|
277
|
+
在 `d19ac72` 上最近一次完整检查结果:
|
|
248
278
|
|
|
249
279
|
```text
|
|
250
280
|
pnpm format:check 通过
|
|
251
281
|
pnpm lint 通过
|
|
252
282
|
pnpm typecheck 通过
|
|
253
|
-
pnpm test 25 个测试文件通过;
|
|
283
|
+
pnpm test 25 个测试文件通过;281 passed,2 skipped
|
|
254
284
|
pnpm build core、web-components、runtime、vanilla 全部通过
|
|
255
285
|
```
|
|
256
286
|
|
|
@@ -258,7 +288,7 @@ pnpm build core、web-components、runtime、vanilla 全部通过
|
|
|
258
288
|
|
|
259
289
|
```text
|
|
260
290
|
/Users/gaostudio/uni-base/uniplat-ai-employee-client-0.14.0.tgz
|
|
261
|
-
/Users/gaostudio/uni-base/uniplat-ai-employee-web-components-0.
|
|
291
|
+
/Users/gaostudio/uni-base/uniplat-ai-employee-web-components-0.25.0.tgz
|
|
262
292
|
```
|
|
263
293
|
|
|
264
294
|
根包本地产物:
|
|
@@ -291,9 +321,9 @@ pnpm --filter @uniplat/ai-employee-web-components pack
|
|
|
291
321
|
|
|
292
322
|
```text
|
|
293
323
|
Manifest: https://10.10.10.131:8787/current.json
|
|
294
|
-
Version: 0.
|
|
295
|
-
Module: https://10.10.10.131:8787/releases/0.
|
|
296
|
-
SHA-256:
|
|
324
|
+
Version: 0.25.0
|
|
325
|
+
Module: https://10.10.10.131:8787/releases/0.25.0/index.js
|
|
326
|
+
SHA-256: 3a37ff4225d346b9f32bdc1ee725a9050f5e0b631520f387407067bca6384631
|
|
297
327
|
```
|
|
298
328
|
|
|
299
329
|
最后验证结果:
|
|
@@ -301,7 +331,7 @@ SHA-256: 2cc1ba9ab48eb20e23cd1f95e68ca9d70413c602aa5f400828147020d4cc0341
|
|
|
301
331
|
- `current.json`:HTTP 200、`Cache-Control: no-store`;
|
|
302
332
|
- 版本模块:HTTP 200、CORS `*`、`immutable`;
|
|
303
333
|
- 远端模块 hash 与本地一致;
|
|
304
|
-
- `current.json` 指向 `./releases/0.
|
|
334
|
+
- `current.json` 指向 `./releases/0.25.0/index.js`。
|
|
305
335
|
|
|
306
336
|
静态服务未启动时可执行:
|
|
307
337
|
|
|
@@ -323,13 +353,13 @@ https://registry.npmjs.org/
|
|
|
323
353
|
|
|
324
354
|
```text
|
|
325
355
|
npm whoami csntgao(已登录)
|
|
326
|
-
@csntgao/uni-base 0.
|
|
356
|
+
@csntgao/uni-base 0.2.0 已发布并回读校验
|
|
327
357
|
@uniplat/ai-employee-client 未发布,PUT 返回 404(无 scope 创建权限)
|
|
328
358
|
@uniplat/ai-employee-web-components 未发布,同上
|
|
329
359
|
```
|
|
330
360
|
|
|
331
|
-
`@csntgao/uni-base@0.
|
|
332
|
-
|
|
361
|
+
`@csntgao/uni-base@0.2.0` 的远端 shasum 为 `b02599ed1443de9909b7e005767ecc045e7a67db`,
|
|
362
|
+
与本地打包一致。发布后远端 `npm view` 有几秒传播延迟,第一次回读到旧版本要重试再确认。注意该包没有 `main`/`exports`/`files`,发布内容是 91 个源码文件,
|
|
333
363
|
不是可直接 `import` 的库;调用方仍应使用 runtime 或 workspace 包。
|
|
334
364
|
|
|
335
365
|
发布需要浏览器 2FA。本机 shell 不是交互终端,直接 `npm publish` 会以 `EOTP` 退出;
|