@csntgao/uni-base 0.1.0
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/.prettierignore +4 -0
- package/.prettierrc.json +6 -0
- package/AGENTS.md +519 -0
- package/CLAUDE.md +512 -0
- package/README.md +734 -0
- package/docs//346/225/260/345/255/227/345/221/230/345/267/245/345/256/214/346/225/264/346/212/275/345/261/211/346/216/245/345/205/245/350/257/264/346/230/216.md +110 -0
- package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/215/225/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +79 -0
- package/docs//347/273/204/347/273/207/345/221/230/345/267/245/345/244/232/351/200/211/347/273/204/344/273/266/346/216/245/345/205/245/350/257/264/346/230/216.md +93 -0
- package/docs//350/264/246/346/210/267/344/270/216/347/231/273/345/275/225/347/273/204/344/273/266/345/212/250/346/200/201/346/216/245/345/205/245/350/257/264/346/230/216.md +201 -0
- package/eslint.config.js +30 -0
- package/examples/vanilla/index.html +33 -0
- package/examples/vanilla/main.js +247 -0
- package/examples/vanilla/package.json +15 -0
- package/examples/vanilla/public/runtime-config.js +10 -0
- package/examples/vanilla/style.css +99 -0
- package/package.json +31 -0
- package/packages/core/package.json +30 -0
- package/packages/core/src/client.ts +1132 -0
- package/packages/core/src/customer-support-client.ts +825 -0
- package/packages/core/src/customer-support-types.ts +123 -0
- package/packages/core/src/index.ts +79 -0
- package/packages/core/src/notification-center-client.ts +462 -0
- package/packages/core/src/notification-center-types.ts +154 -0
- package/packages/core/src/protocol.ts +237 -0
- package/packages/core/src/types.ts +170 -0
- package/packages/core/tests/client.test.ts +760 -0
- package/packages/core/tests/customer-support-client.test.ts +576 -0
- package/packages/core/tests/fake-websocket.ts +55 -0
- package/packages/core/tests/notification-center-client.test.ts +467 -0
- package/packages/core/tests/protocol.test.ts +124 -0
- package/packages/core/tsconfig.json +7 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/web-components/package.json +35 -0
- package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
- package/packages/web-components/src/account-context.ts +1540 -0
- package/packages/web-components/src/application-switcher-transport.ts +243 -0
- package/packages/web-components/src/application-switcher.ts +1103 -0
- package/packages/web-components/src/chat.ts +1754 -0
- package/packages/web-components/src/customer-support-chat.ts +862 -0
- package/packages/web-components/src/customer-support-transport.ts +269 -0
- package/packages/web-components/src/index.ts +256 -0
- package/packages/web-components/src/notification-center.ts +1066 -0
- package/packages/web-components/src/notification-popover.ts +615 -0
- package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
- package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
- package/packages/web-components/src/organization-onboarding.ts +1216 -0
- package/packages/web-components/src/organization-selector.ts +534 -0
- package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
- package/packages/web-components/src/prototype-icons.ts +242 -0
- package/packages/web-components/src/safe-image-url.ts +29 -0
- package/packages/web-components/src/safe-markdown.ts +192 -0
- package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
- package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
- package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
- package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
- package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
- package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
- package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
- package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
- package/packages/web-components/src/user-login-transport.ts +563 -0
- package/packages/web-components/src/user-login.ts +2027 -0
- package/packages/web-components/src/user-menu.ts +880 -0
- package/packages/web-components/tests/account-context.test.ts +798 -0
- package/packages/web-components/tests/application-switcher.test.ts +562 -0
- package/packages/web-components/tests/chat.test.ts +1130 -0
- package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
- package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
- package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
- package/packages/web-components/tests/notification-center.test.ts +429 -0
- package/packages/web-components/tests/notification-popover.test.ts +283 -0
- package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
- package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
- package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
- package/packages/web-components/tests/organization-selector.test.ts +127 -0
- package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
- package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
- package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
- package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
- package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
- package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
- package/packages/web-components/tests/user-login-transport.test.ts +443 -0
- package/packages/web-components/tests/user-login.test.ts +626 -0
- package/packages/web-components/tests/user-menu.test.ts +271 -0
- package/packages/web-components/tsconfig.json +8 -0
- package/packages/web-components/tsup.config.ts +12 -0
- package/packages/web-components/tsup.runtime.config.ts +37 -0
- package/pnpm-workspace.yaml +6 -0
- package/tsconfig.base.json +21 -0
- package/vitest.config.ts +20 -0
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
|
@@ -0,0 +1,1540 @@
|
|
|
1
|
+
import { LitElement, css, html, nothing } from 'lit'
|
|
2
|
+
import type {
|
|
3
|
+
ApplicationLaunchResult,
|
|
4
|
+
ApplicationSwitcherConfig,
|
|
5
|
+
} from './application-switcher-transport'
|
|
6
|
+
import { UniplatApplicationSwitcher } from './application-switcher'
|
|
7
|
+
import {
|
|
8
|
+
INTERNAL_ORGANIZATION_ONBOARDING_TAG,
|
|
9
|
+
type InternalOrganizationOnboarding,
|
|
10
|
+
} from './organization-onboarding'
|
|
11
|
+
import type { OrganizationOnboardingTransport } from './organization-onboarding-transport'
|
|
12
|
+
import type { EnterpriseVerificationStatus } from './organization-selector'
|
|
13
|
+
import { prototypeIcon, type PrototypeIconName } from './prototype-icons'
|
|
14
|
+
import { normalizeSafeImageUrl } from './safe-image-url'
|
|
15
|
+
import { UNIPLAT_APPLICATION_TICKET_FRAGMENT_KEY } from './uniplat-base-application-ticket'
|
|
16
|
+
import type { UniplatUserMenu, UserMenuConfig } from './user-menu'
|
|
17
|
+
|
|
18
|
+
const INTERNAL_APPLICATION_SWITCHER_TAG = 'uniplat-internal-application-switcher'
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
typeof customElements !== 'undefined' &&
|
|
22
|
+
!customElements.get(INTERNAL_APPLICATION_SWITCHER_TAG)
|
|
23
|
+
) {
|
|
24
|
+
customElements.define(INTERNAL_APPLICATION_SWITCHER_TAG, UniplatApplicationSwitcher)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type AccountContextOrganizationType = 'enterprise' | 'team'
|
|
28
|
+
|
|
29
|
+
export interface AccountContextOrganization {
|
|
30
|
+
id: string
|
|
31
|
+
name: string
|
|
32
|
+
type: AccountContextOrganizationType
|
|
33
|
+
/**
|
|
34
|
+
* @deprecated The Host must pass the server-projected organizations for applicationName.
|
|
35
|
+
* This compatibility field is ignored.
|
|
36
|
+
*/
|
|
37
|
+
applicationNames?: readonly string[]
|
|
38
|
+
avatarText?: string
|
|
39
|
+
employeeDisplayName?: string
|
|
40
|
+
employeeAvatarText?: string
|
|
41
|
+
employeeAvatarUrl?: string
|
|
42
|
+
/** Enterprise name shown in the employee-information dialog. */
|
|
43
|
+
companyName?: string
|
|
44
|
+
/** Current employee's department within this organization. */
|
|
45
|
+
employeeDepartmentName?: string
|
|
46
|
+
/** Current employee's employment state, for example `active`. */
|
|
47
|
+
employeeEmploymentStatus?: string
|
|
48
|
+
verificationStatus?: EnterpriseVerificationStatus
|
|
49
|
+
role?: string
|
|
50
|
+
memberCount?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface AccountContextUser {
|
|
54
|
+
displayName: string
|
|
55
|
+
maskedMobile?: string
|
|
56
|
+
avatarText?: string
|
|
57
|
+
avatarUrl?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AccountContextEmployeeInfo {
|
|
61
|
+
departmentName: string | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AccountContextProjection {
|
|
65
|
+
applicationName: string
|
|
66
|
+
user: AccountContextUser
|
|
67
|
+
currentOrganization: AccountContextOrganization | null
|
|
68
|
+
currentOrganizationId: string | null
|
|
69
|
+
organizations: readonly AccountContextOrganization[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AccountContextTransport {
|
|
73
|
+
load(applicationName: string): Promise<AccountContextProjection>
|
|
74
|
+
/**
|
|
75
|
+
* Loads the current JWT-bound employee's details only when the employee
|
|
76
|
+
* information dialog is opened.
|
|
77
|
+
*/
|
|
78
|
+
loadCurrentEmployeeInfo?(): Promise<AccountContextEmployeeInfo>
|
|
79
|
+
switchOrganization?(
|
|
80
|
+
applicationName: string,
|
|
81
|
+
organizationId: string,
|
|
82
|
+
): Promise<AccountContextProjection>
|
|
83
|
+
/**
|
|
84
|
+
* Opens the current application's personal-account route with a short-lived
|
|
85
|
+
* navigation ticket. The optional argument preserves existing custom
|
|
86
|
+
* transports that do not need the application identity.
|
|
87
|
+
*/
|
|
88
|
+
launchAccountSettings?(applicationName?: string): Promise<ApplicationLaunchResult>
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AccountContextConfig {
|
|
92
|
+
applicationName: string
|
|
93
|
+
/**
|
|
94
|
+
* Loads the user profile and the application-projected organization context.
|
|
95
|
+
* The Host supplies authentication only through the transport's token provider.
|
|
96
|
+
*/
|
|
97
|
+
transport?: AccountContextTransport
|
|
98
|
+
/** @deprecated Prefer transport so the component owns identity-data loading. */
|
|
99
|
+
displayName?: string
|
|
100
|
+
maskedMobile?: string
|
|
101
|
+
avatarText?: string
|
|
102
|
+
avatarUrl?: string
|
|
103
|
+
organization?: AccountContextOrganization
|
|
104
|
+
organizations?: readonly AccountContextOrganization[]
|
|
105
|
+
organizationCount?: number
|
|
106
|
+
showWorkbench?: boolean
|
|
107
|
+
showVerificationStatus?: boolean
|
|
108
|
+
/** Enables the integrated workbench application panel. */
|
|
109
|
+
workbench?: ApplicationSwitcherConfig
|
|
110
|
+
/**
|
|
111
|
+
* Creates enterprises and teams from the account menu's internal onboarding flow.
|
|
112
|
+
* The onboarding UI is intentionally not exposed as a standalone custom element.
|
|
113
|
+
*/
|
|
114
|
+
organizationOnboarding?: {
|
|
115
|
+
transport: OrganizationOnboardingTransport
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AccountContextEventMap {
|
|
120
|
+
'account-menu-requested': CustomEvent<{ organizationId: string | null }>
|
|
121
|
+
'account-settings-requested': CustomEvent<Record<string, never>>
|
|
122
|
+
'employee-info-requested': CustomEvent<{ organizationId: string | null }>
|
|
123
|
+
'organization-selected': CustomEvent<{ organizationId: string }>
|
|
124
|
+
'logout-requested': CustomEvent<Record<string, never>>
|
|
125
|
+
'workbench-requested': CustomEvent<{ organizationId: string }>
|
|
126
|
+
'enterprise-authentication-requested': CustomEvent<{
|
|
127
|
+
organizationId: string
|
|
128
|
+
reason: 'unverified' | 'rejected' | 'expired'
|
|
129
|
+
}>
|
|
130
|
+
error: CustomEvent<{
|
|
131
|
+
code: 'LOAD_FAILED' | 'SWITCH_FAILED' | 'ONBOARDING_UNAVAILABLE' | 'ACCOUNT_SETTINGS_FAILED'
|
|
132
|
+
message: string
|
|
133
|
+
}>
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface ResolvedAccountContext {
|
|
137
|
+
displayName: string
|
|
138
|
+
maskedMobile: string
|
|
139
|
+
avatarText: string
|
|
140
|
+
avatarUrl: string
|
|
141
|
+
organization: ResolvedAccountContextOrganization | null
|
|
142
|
+
organizations: readonly ResolvedAccountContextOrganization[]
|
|
143
|
+
organizationCount: number
|
|
144
|
+
showWorkbench: boolean
|
|
145
|
+
showVerificationStatus: boolean
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface ResolvedAccountContextOrganization {
|
|
149
|
+
id: string
|
|
150
|
+
name: string
|
|
151
|
+
type: AccountContextOrganizationType
|
|
152
|
+
avatarText: string
|
|
153
|
+
employeeDisplayName: string
|
|
154
|
+
employeeAvatarText: string
|
|
155
|
+
employeeAvatarUrl: string
|
|
156
|
+
companyName: string
|
|
157
|
+
employeeDepartmentName: string
|
|
158
|
+
employeeEmploymentStatus: string
|
|
159
|
+
verificationStatus: EnterpriseVerificationStatus | null
|
|
160
|
+
role: string
|
|
161
|
+
memberCount: number | null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
type ActivePanel = 'workbench' | 'account' | 'onboarding' | 'employee-info' | null
|
|
165
|
+
|
|
166
|
+
export class UniplatAccountContext extends LitElement {
|
|
167
|
+
static override properties = {
|
|
168
|
+
viewModel: { state: true },
|
|
169
|
+
activePanel: { state: true },
|
|
170
|
+
employeeInfo: { state: true },
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
static override styles = css`
|
|
174
|
+
:host {
|
|
175
|
+
--uniplat-account-context-primary: #2e6bf5;
|
|
176
|
+
--uniplat-account-context-text: #25303f;
|
|
177
|
+
--uniplat-account-context-muted: #7a8794;
|
|
178
|
+
--uniplat-account-context-border: #dce5f0;
|
|
179
|
+
--uniplat-account-context-panel-offset: 10px;
|
|
180
|
+
--uniplat-account-context-font-family:
|
|
181
|
+
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
182
|
+
position: relative;
|
|
183
|
+
display: inline-block;
|
|
184
|
+
color: var(--uniplat-account-context-text);
|
|
185
|
+
font-family: var(--uniplat-account-context-font-family);
|
|
186
|
+
line-height: 1.25;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
* {
|
|
190
|
+
box-sizing: border-box;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
button {
|
|
194
|
+
appearance: none;
|
|
195
|
+
font: inherit;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.context {
|
|
200
|
+
display: inline-flex;
|
|
201
|
+
align-items: center;
|
|
202
|
+
gap: 10px;
|
|
203
|
+
max-width: 100%;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.context.personal {
|
|
207
|
+
gap: 12px;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.create,
|
|
211
|
+
.workbench {
|
|
212
|
+
display: inline-flex;
|
|
213
|
+
align-items: center;
|
|
214
|
+
justify-content: center;
|
|
215
|
+
white-space: nowrap;
|
|
216
|
+
font-size: 13px;
|
|
217
|
+
font-weight: 600;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.create {
|
|
221
|
+
gap: 6px;
|
|
222
|
+
min-height: 30px;
|
|
223
|
+
padding: 0 13px;
|
|
224
|
+
border: 1px solid var(--uniplat-account-context-primary);
|
|
225
|
+
border-radius: 15px;
|
|
226
|
+
color: var(--uniplat-account-context-primary);
|
|
227
|
+
background: #ffffff;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.workbench {
|
|
231
|
+
gap: 6px;
|
|
232
|
+
padding: 9px 15px;
|
|
233
|
+
border: 0;
|
|
234
|
+
border-radius: 19px;
|
|
235
|
+
color: #ffffff;
|
|
236
|
+
background: var(--uniplat-account-context-primary);
|
|
237
|
+
box-shadow: 0 6px 16px rgb(46 107 245 / 25%);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.workbench:hover,
|
|
241
|
+
.workbench:focus-visible,
|
|
242
|
+
.workbench[aria-expanded='true'] {
|
|
243
|
+
outline: none;
|
|
244
|
+
background: #245edc;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.account {
|
|
248
|
+
position: relative;
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 9px;
|
|
252
|
+
min-width: 0;
|
|
253
|
+
height: 42px;
|
|
254
|
+
padding: 0 11px 0 4px;
|
|
255
|
+
border: 1px solid var(--uniplat-account-context-border);
|
|
256
|
+
border-radius: 21px;
|
|
257
|
+
color: inherit;
|
|
258
|
+
background: #f7faff;
|
|
259
|
+
text-align: left;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.account.personal {
|
|
263
|
+
height: 30px;
|
|
264
|
+
padding: 0;
|
|
265
|
+
border: 0;
|
|
266
|
+
border-radius: 0;
|
|
267
|
+
background: transparent;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.account:hover,
|
|
271
|
+
.account:focus-visible,
|
|
272
|
+
.account[aria-expanded='true'] {
|
|
273
|
+
border-color: #bfcfed;
|
|
274
|
+
outline: none;
|
|
275
|
+
background: #f1f5fd;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.account.personal:hover,
|
|
279
|
+
.account.personal:focus-visible {
|
|
280
|
+
border-color: transparent;
|
|
281
|
+
background: transparent;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.attention-dot {
|
|
285
|
+
position: absolute;
|
|
286
|
+
top: -2px;
|
|
287
|
+
right: 5px;
|
|
288
|
+
width: 7px;
|
|
289
|
+
height: 7px;
|
|
290
|
+
border: 2px solid #ffffff;
|
|
291
|
+
border-radius: 50%;
|
|
292
|
+
background: #e5484d;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.avatar {
|
|
296
|
+
position: relative;
|
|
297
|
+
display: grid;
|
|
298
|
+
place-items: center;
|
|
299
|
+
width: 32px;
|
|
300
|
+
height: 32px;
|
|
301
|
+
flex: none;
|
|
302
|
+
border-radius: 50%;
|
|
303
|
+
color: #ffffff;
|
|
304
|
+
background: #233043;
|
|
305
|
+
font-size: 12.5px;
|
|
306
|
+
font-weight: 700;
|
|
307
|
+
overflow: hidden;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.avatar-fallback {
|
|
311
|
+
display: grid;
|
|
312
|
+
width: 100%;
|
|
313
|
+
height: 100%;
|
|
314
|
+
place-items: center;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
.avatar-image {
|
|
318
|
+
position: absolute;
|
|
319
|
+
inset: 0;
|
|
320
|
+
display: block;
|
|
321
|
+
width: 100%;
|
|
322
|
+
height: 100%;
|
|
323
|
+
border-radius: inherit;
|
|
324
|
+
object-fit: cover;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.avatar-image[hidden] {
|
|
328
|
+
display: none;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.copy {
|
|
332
|
+
display: flex;
|
|
333
|
+
min-width: 0;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
line-height: 1.3;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.display-name {
|
|
339
|
+
overflow: hidden;
|
|
340
|
+
max-width: 120px;
|
|
341
|
+
font-size: 12.5px;
|
|
342
|
+
font-weight: 700;
|
|
343
|
+
text-overflow: ellipsis;
|
|
344
|
+
white-space: nowrap;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.organization-line {
|
|
348
|
+
display: inline-flex;
|
|
349
|
+
align-items: center;
|
|
350
|
+
gap: 4px;
|
|
351
|
+
max-width: 212px;
|
|
352
|
+
margin-top: 1px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.organization-name {
|
|
356
|
+
min-width: 0;
|
|
357
|
+
overflow: hidden;
|
|
358
|
+
color: var(--uniplat-account-context-muted);
|
|
359
|
+
font-size: 11px;
|
|
360
|
+
text-overflow: ellipsis;
|
|
361
|
+
white-space: nowrap;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.organization-name.normal {
|
|
365
|
+
color: var(--uniplat-account-context-text);
|
|
366
|
+
font-weight: 600;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.status-icon {
|
|
370
|
+
display: grid;
|
|
371
|
+
width: 17px;
|
|
372
|
+
height: 17px;
|
|
373
|
+
flex: none;
|
|
374
|
+
place-items: center;
|
|
375
|
+
border-radius: 5px;
|
|
376
|
+
font-size: 10px;
|
|
377
|
+
font-weight: 800;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.status-icon.verified,
|
|
381
|
+
.status-icon.team {
|
|
382
|
+
color: #0ca678;
|
|
383
|
+
background: #e3f7ef;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.status-icon.pending {
|
|
387
|
+
color: #1971c2;
|
|
388
|
+
background: #e2effb;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.status-icon.unverified,
|
|
392
|
+
.status-icon.expired {
|
|
393
|
+
color: #e8590c;
|
|
394
|
+
background: #fff4e2;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.status-icon.rejected {
|
|
398
|
+
color: #e5484d;
|
|
399
|
+
background: #fdeced;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.status-icon svg {
|
|
403
|
+
width: 12px;
|
|
404
|
+
height: 12px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.count {
|
|
408
|
+
display: inline-flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
justify-content: center;
|
|
411
|
+
flex: none;
|
|
412
|
+
padding: 1px 6px;
|
|
413
|
+
border-radius: 9px;
|
|
414
|
+
color: var(--uniplat-account-context-primary);
|
|
415
|
+
background: #e6eefe;
|
|
416
|
+
font-size: 11px;
|
|
417
|
+
font-weight: 700;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.selector {
|
|
421
|
+
width: 14px;
|
|
422
|
+
height: 14px;
|
|
423
|
+
flex: none;
|
|
424
|
+
color: #8b98a6;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.floating-panel {
|
|
428
|
+
position: absolute;
|
|
429
|
+
z-index: 1000;
|
|
430
|
+
top: calc(100% + var(--uniplat-account-context-panel-offset));
|
|
431
|
+
right: 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.account-panel {
|
|
435
|
+
width: min(340px, calc(100vw - 24px));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.workbench-panel {
|
|
439
|
+
width: min(328px, calc(100vw - 24px));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.onboarding-panel {
|
|
443
|
+
width: min(400px, calc(100vw - 24px));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.employee-info-modal {
|
|
447
|
+
position: fixed;
|
|
448
|
+
z-index: 1100;
|
|
449
|
+
inset: 0;
|
|
450
|
+
display: grid;
|
|
451
|
+
width: 100vw;
|
|
452
|
+
max-width: none;
|
|
453
|
+
height: 100dvh;
|
|
454
|
+
max-height: none;
|
|
455
|
+
place-items: center;
|
|
456
|
+
margin: 0;
|
|
457
|
+
padding: 24px;
|
|
458
|
+
border: 0;
|
|
459
|
+
background: transparent;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.employee-info-modal::backdrop {
|
|
463
|
+
background: rgb(20 35 56 / 56%);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.employee-info-modal:not([open]) {
|
|
467
|
+
display: none;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.employee-info-dialog {
|
|
471
|
+
width: min(560px, calc(100vw - 32px));
|
|
472
|
+
overflow: hidden;
|
|
473
|
+
border-radius: 16px;
|
|
474
|
+
background: #ffffff;
|
|
475
|
+
box-shadow: 0 24px 60px rgb(13 31 56 / 26%);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.employee-info-header {
|
|
479
|
+
display: flex;
|
|
480
|
+
align-items: center;
|
|
481
|
+
gap: 12px;
|
|
482
|
+
padding: 22px 24px;
|
|
483
|
+
border-bottom: 1px solid #e6edf6;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.employee-info-icon {
|
|
487
|
+
display: grid;
|
|
488
|
+
width: 40px;
|
|
489
|
+
height: 40px;
|
|
490
|
+
flex: none;
|
|
491
|
+
place-items: center;
|
|
492
|
+
border-radius: 11px;
|
|
493
|
+
color: #2e6bf5;
|
|
494
|
+
background: #eaf1ff;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.employee-info-icon svg {
|
|
498
|
+
width: 21px;
|
|
499
|
+
height: 21px;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.employee-info-heading {
|
|
503
|
+
min-width: 0;
|
|
504
|
+
flex: 1;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.employee-info-title {
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
color: #25303f;
|
|
510
|
+
font-size: 17px;
|
|
511
|
+
font-weight: 700;
|
|
512
|
+
text-overflow: ellipsis;
|
|
513
|
+
white-space: nowrap;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.employee-info-company {
|
|
517
|
+
overflow: hidden;
|
|
518
|
+
margin-top: 4px;
|
|
519
|
+
color: #8997a8;
|
|
520
|
+
font-size: 12px;
|
|
521
|
+
text-overflow: ellipsis;
|
|
522
|
+
white-space: nowrap;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.employment-status {
|
|
526
|
+
display: inline-flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
gap: 5px;
|
|
529
|
+
flex: none;
|
|
530
|
+
padding: 5px 9px;
|
|
531
|
+
border-radius: 5px;
|
|
532
|
+
color: #169b68;
|
|
533
|
+
background: #e5f8ef;
|
|
534
|
+
font-size: 12px;
|
|
535
|
+
font-weight: 600;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.employment-status::before {
|
|
539
|
+
width: 5px;
|
|
540
|
+
height: 5px;
|
|
541
|
+
border-radius: 50%;
|
|
542
|
+
background: currentColor;
|
|
543
|
+
content: '';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.employment-status.inactive {
|
|
547
|
+
color: #7a8794;
|
|
548
|
+
background: #eef2f6;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.employee-info-close {
|
|
552
|
+
display: grid;
|
|
553
|
+
width: 28px;
|
|
554
|
+
height: 28px;
|
|
555
|
+
flex: none;
|
|
556
|
+
place-items: center;
|
|
557
|
+
padding: 0;
|
|
558
|
+
border: 0;
|
|
559
|
+
color: #8b98a6;
|
|
560
|
+
background: transparent;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.employee-info-close:hover,
|
|
564
|
+
.employee-info-close:focus-visible {
|
|
565
|
+
border-radius: 6px;
|
|
566
|
+
outline: none;
|
|
567
|
+
color: #526173;
|
|
568
|
+
background: #f1f5f9;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.employee-info-close svg {
|
|
572
|
+
width: 20px;
|
|
573
|
+
height: 20px;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.employee-info-details {
|
|
577
|
+
display: grid;
|
|
578
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
579
|
+
column-gap: 24px;
|
|
580
|
+
padding: 18px 24px;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.employee-info-detail {
|
|
584
|
+
display: flex;
|
|
585
|
+
align-items: center;
|
|
586
|
+
justify-content: space-between;
|
|
587
|
+
gap: 12px;
|
|
588
|
+
min-width: 0;
|
|
589
|
+
min-height: 46px;
|
|
590
|
+
border-bottom: 1px solid #edf1f5;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.employee-info-detail:nth-last-child(-n + 2) {
|
|
594
|
+
border-bottom: 0;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.employee-info-label {
|
|
598
|
+
flex: none;
|
|
599
|
+
color: #95a2b3;
|
|
600
|
+
font-size: 12px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.employee-info-value {
|
|
604
|
+
min-width: 0;
|
|
605
|
+
overflow: hidden;
|
|
606
|
+
color: #25303f;
|
|
607
|
+
font-size: 12px;
|
|
608
|
+
font-weight: 600;
|
|
609
|
+
text-align: right;
|
|
610
|
+
text-overflow: ellipsis;
|
|
611
|
+
white-space: nowrap;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.employee-info-footer {
|
|
615
|
+
padding: 12px 24px 22px;
|
|
616
|
+
color: #9aa7b6;
|
|
617
|
+
font-size: 11px;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
@media (width <= 480px) {
|
|
621
|
+
.employee-info-modal {
|
|
622
|
+
padding: 16px;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.employee-info-header {
|
|
626
|
+
padding: 18px;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.employee-info-details {
|
|
630
|
+
grid-template-columns: 1fr;
|
|
631
|
+
padding: 12px 18px;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.employee-info-detail:nth-last-child(2) {
|
|
635
|
+
border-bottom: 1px solid #edf1f5;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.employee-info-footer {
|
|
639
|
+
padding: 12px 18px 18px;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
uniplat-user-menu,
|
|
644
|
+
uniplat-internal-application-switcher,
|
|
645
|
+
uniplat-internal-organization-onboarding {
|
|
646
|
+
width: 100%;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
svg {
|
|
650
|
+
display: block;
|
|
651
|
+
width: 100%;
|
|
652
|
+
height: 100%;
|
|
653
|
+
fill: none;
|
|
654
|
+
stroke: currentColor;
|
|
655
|
+
stroke-linecap: round;
|
|
656
|
+
stroke-linejoin: round;
|
|
657
|
+
stroke-width: 2;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.create svg,
|
|
661
|
+
.workbench svg {
|
|
662
|
+
width: 14px;
|
|
663
|
+
height: 14px;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.workbench svg:last-child {
|
|
667
|
+
width: 13px;
|
|
668
|
+
height: 13px;
|
|
669
|
+
opacity: 0.8;
|
|
670
|
+
}
|
|
671
|
+
`
|
|
672
|
+
|
|
673
|
+
declare private viewModel: ResolvedAccountContext | null
|
|
674
|
+
declare private activePanel: ActivePanel
|
|
675
|
+
declare private employeeInfo: AccountContextEmployeeInfo | null
|
|
676
|
+
|
|
677
|
+
private workbenchConfig: ApplicationSwitcherConfig | null = null
|
|
678
|
+
private organizationOnboardingTransport: OrganizationOnboardingTransport | null = null
|
|
679
|
+
private applicationName = ''
|
|
680
|
+
private accountTransport: AccountContextTransport | null = null
|
|
681
|
+
private showWorkbench = true
|
|
682
|
+
private showVerificationStatus = true
|
|
683
|
+
private loadGeneration = 0
|
|
684
|
+
private configuredApplicationSwitcher: UniplatApplicationSwitcher | null = null
|
|
685
|
+
private configuredUserMenu: UniplatUserMenu | null = null
|
|
686
|
+
private configuredOrganizationOnboarding: InternalOrganizationOnboarding | null = null
|
|
687
|
+
private accountSettingsNavigationInFlight = false
|
|
688
|
+
private employeeInfoGeneration = 0
|
|
689
|
+
|
|
690
|
+
constructor() {
|
|
691
|
+
super()
|
|
692
|
+
this.viewModel = null
|
|
693
|
+
this.activePanel = null
|
|
694
|
+
this.employeeInfo = null
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
configure(config: AccountContextConfig): void {
|
|
698
|
+
if (!config) throw new TypeError('account-context requires a config object')
|
|
699
|
+
this.applicationName = normalizeRequiredText(config.applicationName, 'Application name', 128)
|
|
700
|
+
this.accountTransport = normalizeAccountTransport(config.transport)
|
|
701
|
+
this.showWorkbench = config.showWorkbench ?? true
|
|
702
|
+
this.showVerificationStatus = config.showVerificationStatus ?? true
|
|
703
|
+
this.workbenchConfig = normalizeWorkbench(config.workbench)
|
|
704
|
+
this.organizationOnboardingTransport = normalizeOrganizationOnboardingTransport(
|
|
705
|
+
config.organizationOnboarding?.transport,
|
|
706
|
+
)
|
|
707
|
+
this.configuredApplicationSwitcher = null
|
|
708
|
+
this.configuredUserMenu = null
|
|
709
|
+
this.configuredOrganizationOnboarding = null
|
|
710
|
+
this.activePanel = null
|
|
711
|
+
this.employeeInfo = null
|
|
712
|
+
this.employeeInfoGeneration += 1
|
|
713
|
+
this.loadGeneration += 1
|
|
714
|
+
if (this.accountTransport) {
|
|
715
|
+
this.viewModel = null
|
|
716
|
+
if (this.isConnected) void this.refresh()
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
this.applyManualConfig(config)
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async refresh(): Promise<void> {
|
|
723
|
+
const transport = this.accountTransport
|
|
724
|
+
if (!transport) return
|
|
725
|
+
const generation = ++this.loadGeneration
|
|
726
|
+
try {
|
|
727
|
+
const projection = await transport.load(this.applicationName)
|
|
728
|
+
if (generation !== this.loadGeneration) return
|
|
729
|
+
this.applyProjection(projection)
|
|
730
|
+
} catch {
|
|
731
|
+
if (generation !== this.loadGeneration) return
|
|
732
|
+
this.dispatchPublicEvent('error', {
|
|
733
|
+
code: 'LOAD_FAILED',
|
|
734
|
+
message: '账户信息加载失败,请稍后重试。',
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async switchOrganization(organizationId: string): Promise<void> {
|
|
740
|
+
const normalizedOrganizationId = normalizeRequiredText(organizationId, 'Organization id', 128)
|
|
741
|
+
const transport = this.accountTransport
|
|
742
|
+
if (!transport?.switchOrganization) {
|
|
743
|
+
throw new Error('Account context organization switching is unavailable')
|
|
744
|
+
}
|
|
745
|
+
const generation = ++this.loadGeneration
|
|
746
|
+
try {
|
|
747
|
+
const projection = await transport.switchOrganization(
|
|
748
|
+
this.applicationName,
|
|
749
|
+
normalizedOrganizationId,
|
|
750
|
+
)
|
|
751
|
+
if (generation !== this.loadGeneration) return
|
|
752
|
+
this.applyProjection(projection)
|
|
753
|
+
this.activePanel = null
|
|
754
|
+
} catch {
|
|
755
|
+
if (generation === this.loadGeneration) {
|
|
756
|
+
this.dispatchPublicEvent('error', {
|
|
757
|
+
code: 'SWITCH_FAILED',
|
|
758
|
+
message: '组织切换失败,当前登录状态未改变。',
|
|
759
|
+
})
|
|
760
|
+
}
|
|
761
|
+
throw new Error('Account context organization switch failed')
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private applyProjection(projection: AccountContextProjection): void {
|
|
766
|
+
if (!projection || typeof projection !== 'object') {
|
|
767
|
+
throw new TypeError('account-context projection must be an object')
|
|
768
|
+
}
|
|
769
|
+
if (projection.applicationName !== this.applicationName) {
|
|
770
|
+
throw new TypeError('account-context projection applicationName does not match')
|
|
771
|
+
}
|
|
772
|
+
const currentOrganizationId = projection.currentOrganizationId
|
|
773
|
+
if (
|
|
774
|
+
(currentOrganizationId === null) !== (projection.currentOrganization === null) ||
|
|
775
|
+
(currentOrganizationId !== null &&
|
|
776
|
+
projection.currentOrganization?.id !== currentOrganizationId)
|
|
777
|
+
) {
|
|
778
|
+
throw new TypeError('account-context projection current organization is inconsistent')
|
|
779
|
+
}
|
|
780
|
+
this.applyManualConfig({
|
|
781
|
+
applicationName: this.applicationName,
|
|
782
|
+
displayName: projection.user.displayName,
|
|
783
|
+
maskedMobile: projection.user.maskedMobile,
|
|
784
|
+
avatarText: projection.user.avatarText,
|
|
785
|
+
avatarUrl: projection.user.avatarUrl,
|
|
786
|
+
organization: projection.currentOrganization ?? undefined,
|
|
787
|
+
organizations: projection.organizations,
|
|
788
|
+
showWorkbench: this.showWorkbench,
|
|
789
|
+
showVerificationStatus: this.showVerificationStatus,
|
|
790
|
+
workbench: this.workbenchConfig ?? undefined,
|
|
791
|
+
organizationOnboarding: this.organizationOnboardingTransport
|
|
792
|
+
? { transport: this.organizationOnboardingTransport }
|
|
793
|
+
: undefined,
|
|
794
|
+
})
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private applyManualConfig(config: AccountContextConfig): void {
|
|
798
|
+
const userDisplayName = normalizeRequiredText(config.displayName, 'Display name', 80)
|
|
799
|
+
const configuredOrganization = normalizeOrganization(config.organization)
|
|
800
|
+
const organizations = normalizeOrganizations(config.organizations, configuredOrganization)
|
|
801
|
+
const organization = configuredOrganization
|
|
802
|
+
? (organizations.find((entry) => entry.id === configuredOrganization.id) ??
|
|
803
|
+
configuredOrganization)
|
|
804
|
+
: null
|
|
805
|
+
const organizationCount = normalizeOrganizationCount(
|
|
806
|
+
config.organizationCount,
|
|
807
|
+
organization,
|
|
808
|
+
organizations.length,
|
|
809
|
+
)
|
|
810
|
+
const userAvatarText =
|
|
811
|
+
normalizeOptionalText(config.avatarText, 4) || Array.from(userDisplayName.trim())[0] || '用'
|
|
812
|
+
const userAvatarUrl = normalizeSafeImageUrl(config.avatarUrl, 'account-context avatar URL')
|
|
813
|
+
const employeeDisplayName = organization?.employeeDisplayName ?? ''
|
|
814
|
+
const displayName = employeeDisplayName || userDisplayName
|
|
815
|
+
this.viewModel = {
|
|
816
|
+
displayName,
|
|
817
|
+
maskedMobile: normalizeMaskedMobile(config.maskedMobile),
|
|
818
|
+
avatarText: employeeDisplayName
|
|
819
|
+
? organization?.employeeAvatarText || Array.from(employeeDisplayName)[0] || '员'
|
|
820
|
+
: userAvatarText,
|
|
821
|
+
avatarUrl: employeeDisplayName ? (organization?.employeeAvatarUrl ?? '') : userAvatarUrl,
|
|
822
|
+
organization,
|
|
823
|
+
organizations,
|
|
824
|
+
organizationCount,
|
|
825
|
+
// An open application can have oid=0 while still exposing a global workbench.
|
|
826
|
+
// Whether that workbench is available is determined by the configured capability,
|
|
827
|
+
// not by the presence of a selected organization.
|
|
828
|
+
showWorkbench: config.showWorkbench ?? true,
|
|
829
|
+
showVerificationStatus: organization ? (config.showVerificationStatus ?? true) : false,
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
override connectedCallback(): void {
|
|
834
|
+
super.connectedCallback()
|
|
835
|
+
document.addEventListener('pointerdown', this.handleDocumentPointerDown, true)
|
|
836
|
+
document.addEventListener('keydown', this.handleDocumentKeyDown)
|
|
837
|
+
if (this.accountTransport && !this.viewModel) void this.refresh()
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
override disconnectedCallback(): void {
|
|
841
|
+
document.removeEventListener('pointerdown', this.handleDocumentPointerDown, true)
|
|
842
|
+
document.removeEventListener('keydown', this.handleDocumentKeyDown)
|
|
843
|
+
this.activePanel = null
|
|
844
|
+
this.employeeInfo = null
|
|
845
|
+
this.loadGeneration += 1
|
|
846
|
+
this.employeeInfoGeneration += 1
|
|
847
|
+
this.configuredApplicationSwitcher = null
|
|
848
|
+
this.configuredUserMenu = null
|
|
849
|
+
this.configuredOrganizationOnboarding = null
|
|
850
|
+
super.disconnectedCallback()
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
override render() {
|
|
854
|
+
const viewModel = this.viewModel
|
|
855
|
+
if (!viewModel) return nothing
|
|
856
|
+
const organization = viewModel.organization
|
|
857
|
+
const hasAvailableOrganizations = viewModel.organizations.length > 0
|
|
858
|
+
return html`
|
|
859
|
+
<div
|
|
860
|
+
class="context ${organization ? 'organization' : 'personal'}"
|
|
861
|
+
part="context"
|
|
862
|
+
aria-label="账户、工作台与当前组织"
|
|
863
|
+
>
|
|
864
|
+
${
|
|
865
|
+
viewModel.showWorkbench && (organization || this.workbenchConfig)
|
|
866
|
+
? html`<button
|
|
867
|
+
class="workbench"
|
|
868
|
+
part="workbench-action"
|
|
869
|
+
type="button"
|
|
870
|
+
aria-haspopup="menu"
|
|
871
|
+
aria-expanded=${String(this.activePanel === 'workbench')}
|
|
872
|
+
@click=${this.toggleWorkbench}
|
|
873
|
+
>
|
|
874
|
+
${prototypeIcon('layout-grid')}工作台${prototypeIcon('chevron-down')}
|
|
875
|
+
</button>`
|
|
876
|
+
: !hasAvailableOrganizations
|
|
877
|
+
? html`<button
|
|
878
|
+
class="create"
|
|
879
|
+
part="create-action"
|
|
880
|
+
type="button"
|
|
881
|
+
@click=${this.openOrganizationOnboarding}
|
|
882
|
+
>
|
|
883
|
+
${prototypeIcon('plus')}创建团队 / 企业
|
|
884
|
+
</button>`
|
|
885
|
+
: nothing
|
|
886
|
+
}
|
|
887
|
+
<button
|
|
888
|
+
class="account ${organization ? '' : 'personal'}"
|
|
889
|
+
part="account-trigger"
|
|
890
|
+
type="button"
|
|
891
|
+
aria-haspopup="menu"
|
|
892
|
+
aria-expanded=${String(this.activePanel === 'account')}
|
|
893
|
+
@click=${this.toggleAccountMenu}
|
|
894
|
+
>
|
|
895
|
+
${this.needsAttention(organization) ? html`<span class="attention-dot"></span>` : nothing}
|
|
896
|
+
<span class="avatar" part="avatar" aria-hidden="true">
|
|
897
|
+
<span class="avatar-fallback">${viewModel.avatarText}</span>
|
|
898
|
+
${
|
|
899
|
+
viewModel.avatarUrl
|
|
900
|
+
? html`<img
|
|
901
|
+
class="avatar-image"
|
|
902
|
+
part="avatar-image"
|
|
903
|
+
src=${viewModel.avatarUrl}
|
|
904
|
+
alt=""
|
|
905
|
+
referrerpolicy="no-referrer"
|
|
906
|
+
@load=${this.showLoadedAvatar}
|
|
907
|
+
@error=${this.hideBrokenAvatar}
|
|
908
|
+
/>`
|
|
909
|
+
: nothing
|
|
910
|
+
}
|
|
911
|
+
</span>
|
|
912
|
+
<span class="copy">
|
|
913
|
+
<span class="display-name" part="display-name">${viewModel.displayName}</span>
|
|
914
|
+
${
|
|
915
|
+
organization
|
|
916
|
+
? this.renderOrganizationLine(organization, viewModel.showVerificationStatus)
|
|
917
|
+
: nothing
|
|
918
|
+
}
|
|
919
|
+
</span>
|
|
920
|
+
${
|
|
921
|
+
organization && viewModel.organizationCount > 1
|
|
922
|
+
? html`<span class="count" part="organization-count"
|
|
923
|
+
>${viewModel.organizationCount}</span
|
|
924
|
+
>`
|
|
925
|
+
: nothing
|
|
926
|
+
}
|
|
927
|
+
${
|
|
928
|
+
organization
|
|
929
|
+
? html`<span class="selector">${prototypeIcon('selector')}</span>`
|
|
930
|
+
: nothing
|
|
931
|
+
}
|
|
932
|
+
</button>
|
|
933
|
+
</div>
|
|
934
|
+
${this.renderActivePanel()}
|
|
935
|
+
`
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
protected override updated(): void {
|
|
939
|
+
const applicationSwitcher = this.renderRoot.querySelector<UniplatApplicationSwitcher>(
|
|
940
|
+
INTERNAL_APPLICATION_SWITCHER_TAG,
|
|
941
|
+
)
|
|
942
|
+
if (applicationSwitcher && applicationSwitcher !== this.configuredApplicationSwitcher) {
|
|
943
|
+
const workbench = this.workbenchConfig
|
|
944
|
+
if (workbench) {
|
|
945
|
+
applicationSwitcher.configure({
|
|
946
|
+
...workbench,
|
|
947
|
+
presentation: 'embedded',
|
|
948
|
+
showOrganizationSelector: false,
|
|
949
|
+
})
|
|
950
|
+
this.configuredApplicationSwitcher = applicationSwitcher
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (!applicationSwitcher) this.configuredApplicationSwitcher = null
|
|
954
|
+
|
|
955
|
+
const userMenu = this.renderRoot.querySelector<UniplatUserMenu>('uniplat-user-menu')
|
|
956
|
+
if (userMenu && userMenu !== this.configuredUserMenu && this.viewModel) {
|
|
957
|
+
userMenu.configure(this.toUserMenuConfig(this.viewModel))
|
|
958
|
+
this.configuredUserMenu = userMenu
|
|
959
|
+
}
|
|
960
|
+
if (!userMenu) this.configuredUserMenu = null
|
|
961
|
+
|
|
962
|
+
const onboarding = this.renderRoot.querySelector<InternalOrganizationOnboarding>(
|
|
963
|
+
INTERNAL_ORGANIZATION_ONBOARDING_TAG,
|
|
964
|
+
)
|
|
965
|
+
if (onboarding && onboarding !== this.configuredOrganizationOnboarding) {
|
|
966
|
+
const transport = this.organizationOnboardingTransport
|
|
967
|
+
if (transport) {
|
|
968
|
+
onboarding.configure({ transport, initialStep: 'method-selection' })
|
|
969
|
+
this.configuredOrganizationOnboarding = onboarding
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (!onboarding) this.configuredOrganizationOnboarding = null
|
|
973
|
+
|
|
974
|
+
const employeeInfoModal = this.renderRoot.querySelector<HTMLDialogElement>(
|
|
975
|
+
'dialog.employee-info-modal',
|
|
976
|
+
)
|
|
977
|
+
if (employeeInfoModal && !employeeInfoModal.open) {
|
|
978
|
+
try {
|
|
979
|
+
employeeInfoModal.showModal()
|
|
980
|
+
} catch {
|
|
981
|
+
employeeInfoModal.setAttribute('open', '')
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
private renderOrganizationLine(
|
|
987
|
+
organization: ResolvedAccountContextOrganization,
|
|
988
|
+
showVerificationStatus: boolean,
|
|
989
|
+
) {
|
|
990
|
+
const status = showVerificationStatus ? organizationStatus(organization) : null
|
|
991
|
+
return html`
|
|
992
|
+
<span class="organization-line">
|
|
993
|
+
<span
|
|
994
|
+
class=${`organization-name${status === 'verified' || status === 'team' ? ' normal' : ''}`}
|
|
995
|
+
part="organization-name"
|
|
996
|
+
>${organization.name}</span
|
|
997
|
+
>
|
|
998
|
+
${
|
|
999
|
+
status
|
|
1000
|
+
? html`<span
|
|
1001
|
+
class=${`status-icon ${status}`}
|
|
1002
|
+
part="verification-status"
|
|
1003
|
+
title=${organizationStatusDescription(status)}
|
|
1004
|
+
aria-label=${organizationStatusDescription(status)}
|
|
1005
|
+
>${prototypeIcon(organizationStatusIcon(status))}</span
|
|
1006
|
+
>`
|
|
1007
|
+
: nothing
|
|
1008
|
+
}
|
|
1009
|
+
</span>
|
|
1010
|
+
`
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private renderActivePanel() {
|
|
1014
|
+
if (this.activePanel === 'employee-info') return this.renderEmployeeInfoDialog()
|
|
1015
|
+
if (this.activePanel === 'workbench' && this.workbenchConfig) {
|
|
1016
|
+
return html`<div
|
|
1017
|
+
class="floating-panel workbench-panel"
|
|
1018
|
+
part="workbench-panel"
|
|
1019
|
+
@application-selected=${this.closePanelAfterAction}
|
|
1020
|
+
>
|
|
1021
|
+
<uniplat-internal-application-switcher></uniplat-internal-application-switcher>
|
|
1022
|
+
</div>`
|
|
1023
|
+
}
|
|
1024
|
+
if (this.activePanel === 'onboarding' && this.organizationOnboardingTransport) {
|
|
1025
|
+
return html`<div
|
|
1026
|
+
class="floating-panel onboarding-panel"
|
|
1027
|
+
part="onboarding-panel"
|
|
1028
|
+
@onboarding-skipped=${this.closePanelAfterAction}
|
|
1029
|
+
@organization-created=${this.refreshAfterOrganizationCreated}
|
|
1030
|
+
>
|
|
1031
|
+
<uniplat-internal-organization-onboarding></uniplat-internal-organization-onboarding>
|
|
1032
|
+
</div>`
|
|
1033
|
+
}
|
|
1034
|
+
if (this.activePanel === 'account') {
|
|
1035
|
+
return html`<div
|
|
1036
|
+
class="floating-panel account-panel"
|
|
1037
|
+
part="account-panel"
|
|
1038
|
+
@account-settings-requested=${this.handleAccountSettingsRequest}
|
|
1039
|
+
@employee-info-requested=${this.openEmployeeInfo}
|
|
1040
|
+
@organization-selected=${this.closePanelAfterAction}
|
|
1041
|
+
@logout-requested=${this.closePanelAfterAction}
|
|
1042
|
+
>
|
|
1043
|
+
<uniplat-user-menu></uniplat-user-menu>
|
|
1044
|
+
</div>`
|
|
1045
|
+
}
|
|
1046
|
+
return nothing
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
private renderEmployeeInfoDialog() {
|
|
1050
|
+
const viewModel = this.viewModel
|
|
1051
|
+
const organization = viewModel?.organization
|
|
1052
|
+
if (!viewModel || !organization) return nothing
|
|
1053
|
+
const employmentStatus = employeeEmploymentStatus(organization.employeeEmploymentStatus)
|
|
1054
|
+
const employeeName = organization.employeeDisplayName || viewModel.displayName
|
|
1055
|
+
return html`<dialog
|
|
1056
|
+
class="employee-info-modal"
|
|
1057
|
+
part="employee-info-modal"
|
|
1058
|
+
@click=${this.closeEmployeeInfo}
|
|
1059
|
+
@cancel=${this.closeEmployeeInfo}
|
|
1060
|
+
>
|
|
1061
|
+
<section
|
|
1062
|
+
class="employee-info-dialog"
|
|
1063
|
+
part="employee-info-dialog"
|
|
1064
|
+
role="dialog"
|
|
1065
|
+
aria-modal="true"
|
|
1066
|
+
aria-labelledby="uniplat-employee-info-title"
|
|
1067
|
+
@click=${stopEventPropagation}
|
|
1068
|
+
>
|
|
1069
|
+
<header class="employee-info-header">
|
|
1070
|
+
<span class="employee-info-icon" aria-hidden="true">${prototypeIcon('id-badge-2')}</span>
|
|
1071
|
+
<span class="employee-info-heading">
|
|
1072
|
+
<span class="employee-info-title" id="uniplat-employee-info-title">我的员工信息</span>
|
|
1073
|
+
<span class="employee-info-company"
|
|
1074
|
+
>${organization.companyName || organization.name}</span
|
|
1075
|
+
>
|
|
1076
|
+
</span>
|
|
1077
|
+
<span class=${`employment-status ${employmentStatus.className}`}
|
|
1078
|
+
>${employmentStatus.label}</span
|
|
1079
|
+
>
|
|
1080
|
+
<button
|
|
1081
|
+
class="employee-info-close"
|
|
1082
|
+
part="employee-info-close"
|
|
1083
|
+
type="button"
|
|
1084
|
+
aria-label="关闭员工信息"
|
|
1085
|
+
@click=${this.closeEmployeeInfo}
|
|
1086
|
+
>
|
|
1087
|
+
${prototypeIcon('x')}
|
|
1088
|
+
</button>
|
|
1089
|
+
</header>
|
|
1090
|
+
<div class="employee-info-details" part="employee-info-details">
|
|
1091
|
+
${this.renderEmployeeInfoDetail('所属企业', organization.companyName || organization.name)}
|
|
1092
|
+
${this.renderEmployeeInfoDetail(
|
|
1093
|
+
'部门',
|
|
1094
|
+
this.employeeInfo === null ? '加载中…' : (this.employeeInfo.departmentName ?? '未设置'),
|
|
1095
|
+
)}
|
|
1096
|
+
${this.renderEmployeeInfoDetail('姓名', employeeName)}
|
|
1097
|
+
${this.renderEmployeeInfoDetail('我的角色', organization.role || '成员')}
|
|
1098
|
+
</div>
|
|
1099
|
+
<footer class="employee-info-footer">
|
|
1100
|
+
员工信息由企业管理员维护,如有误请联系管理员修改。
|
|
1101
|
+
</footer>
|
|
1102
|
+
</section>
|
|
1103
|
+
</dialog>`
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
private renderEmployeeInfoDetail(label: string, value: string) {
|
|
1107
|
+
return html`<div class="employee-info-detail">
|
|
1108
|
+
<span class="employee-info-label">${label}</span>
|
|
1109
|
+
<span class="employee-info-value">${value}</span>
|
|
1110
|
+
</div>`
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private toUserMenuConfig(viewModel: ResolvedAccountContext): UserMenuConfig {
|
|
1114
|
+
return {
|
|
1115
|
+
displayName: viewModel.displayName,
|
|
1116
|
+
maskedMobile: viewModel.maskedMobile,
|
|
1117
|
+
avatarText: viewModel.avatarText,
|
|
1118
|
+
avatarUrl: viewModel.avatarUrl,
|
|
1119
|
+
currentOrganizationId: viewModel.organization?.id,
|
|
1120
|
+
organizationCount: viewModel.organizationCount,
|
|
1121
|
+
organizations: viewModel.organizations.map((organization) => ({
|
|
1122
|
+
id: organization.id,
|
|
1123
|
+
name: organization.name,
|
|
1124
|
+
type: organization.type === 'enterprise' ? 'company' : 'team',
|
|
1125
|
+
avatarText: organization.avatarText,
|
|
1126
|
+
role: organization.role,
|
|
1127
|
+
memberCount: organization.memberCount ?? undefined,
|
|
1128
|
+
verificationStatus: organization.verificationStatus ?? undefined,
|
|
1129
|
+
})),
|
|
1130
|
+
onCreateOrganization: this.openOrganizationOnboarding,
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private needsAttention(organization: ResolvedAccountContextOrganization | null): boolean {
|
|
1135
|
+
return (
|
|
1136
|
+
organization?.type === 'enterprise' &&
|
|
1137
|
+
['rejected', 'expired'].includes(organization.verificationStatus ?? '')
|
|
1138
|
+
)
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
private readonly toggleWorkbench = (): void => {
|
|
1142
|
+
const organizationId = this.viewModel?.organization?.id
|
|
1143
|
+
// Preserve the existing event contract for organization-bound workbenches.
|
|
1144
|
+
// An oid=0 open application has no organization id to expose, but can still
|
|
1145
|
+
// open its own global application panel.
|
|
1146
|
+
if (organizationId) this.dispatchPublicEvent('workbench-requested', { organizationId })
|
|
1147
|
+
if (this.workbenchConfig) {
|
|
1148
|
+
this.activePanel = this.activePanel === 'workbench' ? null : 'workbench'
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private readonly toggleAccountMenu = (): void => {
|
|
1153
|
+
this.dispatchPublicEvent('account-menu-requested', {
|
|
1154
|
+
organizationId: this.viewModel?.organization?.id ?? null,
|
|
1155
|
+
})
|
|
1156
|
+
this.activePanel = this.activePanel === 'account' ? null : 'account'
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
startOrganizationOnboarding(): void {
|
|
1160
|
+
if (!this.organizationOnboardingTransport) {
|
|
1161
|
+
this.dispatchPublicEvent('error', {
|
|
1162
|
+
code: 'ONBOARDING_UNAVAILABLE',
|
|
1163
|
+
message: '暂时无法启动创建团队或企业流程。',
|
|
1164
|
+
})
|
|
1165
|
+
return
|
|
1166
|
+
}
|
|
1167
|
+
this.activePanel = 'onboarding'
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private readonly openOrganizationOnboarding = (): void => {
|
|
1171
|
+
this.startOrganizationOnboarding()
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
private readonly refreshAfterOrganizationCreated = (): void => {
|
|
1175
|
+
void this.refresh()
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private readonly closePanelAfterAction = (): void => {
|
|
1179
|
+
this.activePanel = null
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
private readonly openEmployeeInfo = (): void => {
|
|
1183
|
+
if (!this.viewModel?.organization) {
|
|
1184
|
+
this.activePanel = null
|
|
1185
|
+
return
|
|
1186
|
+
}
|
|
1187
|
+
this.activePanel = 'employee-info'
|
|
1188
|
+
this.employeeInfo = null
|
|
1189
|
+
void this.loadCurrentEmployeeInfo()
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
private readonly closeEmployeeInfo = (): void => {
|
|
1193
|
+
this.activePanel = null
|
|
1194
|
+
this.employeeInfoGeneration += 1
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
private async loadCurrentEmployeeInfo(): Promise<void> {
|
|
1198
|
+
const loader = this.accountTransport?.loadCurrentEmployeeInfo
|
|
1199
|
+
if (!loader) {
|
|
1200
|
+
this.employeeInfo = { departmentName: null }
|
|
1201
|
+
return
|
|
1202
|
+
}
|
|
1203
|
+
const generation = ++this.employeeInfoGeneration
|
|
1204
|
+
try {
|
|
1205
|
+
const employeeInfo = normalizeEmployeeInfo(await loader())
|
|
1206
|
+
if (generation !== this.employeeInfoGeneration || this.activePanel !== 'employee-info') return
|
|
1207
|
+
this.employeeInfo = employeeInfo
|
|
1208
|
+
} catch {
|
|
1209
|
+
if (generation !== this.employeeInfoGeneration || this.activePanel !== 'employee-info') return
|
|
1210
|
+
this.employeeInfo = { departmentName: null }
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
private readonly handleAccountSettingsRequest = (event: Event): void => {
|
|
1215
|
+
const launcher = this.accountTransport?.launchAccountSettings
|
|
1216
|
+
if (!launcher || this.accountSettingsNavigationInFlight) {
|
|
1217
|
+
this.closePanelAfterAction()
|
|
1218
|
+
return
|
|
1219
|
+
}
|
|
1220
|
+
event.stopPropagation()
|
|
1221
|
+
this.activePanel = null
|
|
1222
|
+
this.accountSettingsNavigationInFlight = true
|
|
1223
|
+
const accountSettingsWindow = openPendingAccountSettingsWindow()
|
|
1224
|
+
void this.openAccountSettings(launcher, accountSettingsWindow)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private async openAccountSettings(
|
|
1228
|
+
launcher: NonNullable<AccountContextTransport['launchAccountSettings']>,
|
|
1229
|
+
accountSettingsWindow: Window | null,
|
|
1230
|
+
): Promise<void> {
|
|
1231
|
+
let pendingWindow = accountSettingsWindow
|
|
1232
|
+
try {
|
|
1233
|
+
const launch = await launcher(this.applicationName)
|
|
1234
|
+
navigateToAccountSettings(launch.url, pendingWindow)
|
|
1235
|
+
pendingWindow = null
|
|
1236
|
+
} catch {
|
|
1237
|
+
this.dispatchPublicEvent('error', {
|
|
1238
|
+
code: 'ACCOUNT_SETTINGS_FAILED',
|
|
1239
|
+
message: '个人账号设置暂时无法打开,请稍后重试。',
|
|
1240
|
+
})
|
|
1241
|
+
} finally {
|
|
1242
|
+
closePendingAccountSettingsWindow(pendingWindow)
|
|
1243
|
+
this.accountSettingsNavigationInFlight = false
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private readonly hideBrokenAvatar = (event: Event): void => {
|
|
1248
|
+
if (event.currentTarget instanceof HTMLImageElement) event.currentTarget.hidden = true
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private readonly showLoadedAvatar = (event: Event): void => {
|
|
1252
|
+
if (event.currentTarget instanceof HTMLImageElement) event.currentTarget.hidden = false
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
private readonly handleDocumentPointerDown = (event: PointerEvent): void => {
|
|
1256
|
+
if (this.activePanel && !event.composedPath().includes(this)) this.activePanel = null
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
private readonly handleDocumentKeyDown = (event: KeyboardEvent): void => {
|
|
1260
|
+
if (event.key !== 'Escape' || !this.activePanel) return
|
|
1261
|
+
const activePanel = this.activePanel
|
|
1262
|
+
this.activePanel = null
|
|
1263
|
+
const selector = activePanel === 'workbench' ? '.workbench' : '.account'
|
|
1264
|
+
void this.updateComplete.then(() =>
|
|
1265
|
+
this.renderRoot.querySelector<HTMLButtonElement>(selector)?.focus(),
|
|
1266
|
+
)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
private dispatchPublicEvent<K extends keyof AccountContextEventMap>(
|
|
1270
|
+
type: K,
|
|
1271
|
+
detail: AccountContextEventMap[K]['detail'],
|
|
1272
|
+
): void {
|
|
1273
|
+
this.dispatchEvent(
|
|
1274
|
+
new CustomEvent(type, {
|
|
1275
|
+
detail,
|
|
1276
|
+
bubbles: true,
|
|
1277
|
+
composed: true,
|
|
1278
|
+
}),
|
|
1279
|
+
)
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function normalizeAccountTransport(
|
|
1284
|
+
transport: AccountContextTransport | undefined,
|
|
1285
|
+
): AccountContextTransport | null {
|
|
1286
|
+
if (transport === undefined) return null
|
|
1287
|
+
if (!transport || typeof transport.load !== 'function') {
|
|
1288
|
+
throw new TypeError('account-context transport requires load()')
|
|
1289
|
+
}
|
|
1290
|
+
if (
|
|
1291
|
+
transport.switchOrganization !== undefined &&
|
|
1292
|
+
typeof transport.switchOrganization !== 'function'
|
|
1293
|
+
) {
|
|
1294
|
+
throw new TypeError('account-context transport switchOrganization must be a function')
|
|
1295
|
+
}
|
|
1296
|
+
if (
|
|
1297
|
+
transport.loadCurrentEmployeeInfo !== undefined &&
|
|
1298
|
+
typeof transport.loadCurrentEmployeeInfo !== 'function'
|
|
1299
|
+
) {
|
|
1300
|
+
throw new TypeError('account-context transport loadCurrentEmployeeInfo must be a function')
|
|
1301
|
+
}
|
|
1302
|
+
if (
|
|
1303
|
+
transport.launchAccountSettings !== undefined &&
|
|
1304
|
+
typeof transport.launchAccountSettings !== 'function'
|
|
1305
|
+
) {
|
|
1306
|
+
throw new TypeError('account-context transport launchAccountSettings must be a function')
|
|
1307
|
+
}
|
|
1308
|
+
return transport
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function normalizeWorkbench(
|
|
1312
|
+
workbench: ApplicationSwitcherConfig | undefined,
|
|
1313
|
+
): ApplicationSwitcherConfig | null {
|
|
1314
|
+
if (workbench === undefined) return null
|
|
1315
|
+
if (!workbench || typeof workbench.getAccessToken !== 'function') {
|
|
1316
|
+
throw new TypeError('account-context workbench requires getAccessToken()')
|
|
1317
|
+
}
|
|
1318
|
+
return workbench
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function normalizeOrganizationOnboardingTransport(
|
|
1322
|
+
transport: OrganizationOnboardingTransport | undefined,
|
|
1323
|
+
): OrganizationOnboardingTransport | null {
|
|
1324
|
+
if (transport === undefined) return null
|
|
1325
|
+
if (
|
|
1326
|
+
!transport ||
|
|
1327
|
+
typeof transport.createEnterprise !== 'function' ||
|
|
1328
|
+
typeof transport.createTeam !== 'function'
|
|
1329
|
+
) {
|
|
1330
|
+
throw new TypeError(
|
|
1331
|
+
'account-context organizationOnboarding requires createEnterprise() and createTeam()',
|
|
1332
|
+
)
|
|
1333
|
+
}
|
|
1334
|
+
return transport
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function normalizeOrganization(
|
|
1338
|
+
organization: AccountContextOrganization | undefined,
|
|
1339
|
+
): ResolvedAccountContextOrganization | null {
|
|
1340
|
+
if (organization === undefined) return null
|
|
1341
|
+
return normalizeOrganizationEntry(organization, 'account-context organization')
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function normalizeOrganizations(
|
|
1345
|
+
organizations: readonly AccountContextOrganization[] | undefined,
|
|
1346
|
+
current: ResolvedAccountContextOrganization | null,
|
|
1347
|
+
): readonly ResolvedAccountContextOrganization[] {
|
|
1348
|
+
if (organizations === undefined) {
|
|
1349
|
+
return current ? Object.freeze([current]) : Object.freeze([])
|
|
1350
|
+
}
|
|
1351
|
+
if (!Array.isArray(organizations)) {
|
|
1352
|
+
throw new TypeError('account-context organizations must be an array')
|
|
1353
|
+
}
|
|
1354
|
+
const allIds = new Set<string>()
|
|
1355
|
+
const normalized = organizations.map((organization, index) => {
|
|
1356
|
+
const entry = normalizeOrganizationEntry(
|
|
1357
|
+
organization,
|
|
1358
|
+
`account-context organization ${index + 1}`,
|
|
1359
|
+
)
|
|
1360
|
+
if (allIds.has(entry.id)) throw new TypeError('account-context organization IDs must be unique')
|
|
1361
|
+
allIds.add(entry.id)
|
|
1362
|
+
return entry
|
|
1363
|
+
})
|
|
1364
|
+
if (current && !allIds.has(current.id)) {
|
|
1365
|
+
throw new TypeError('account-context organizations must include the current organization')
|
|
1366
|
+
}
|
|
1367
|
+
return Object.freeze(normalized)
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function normalizeOrganizationEntry(
|
|
1371
|
+
organization: AccountContextOrganization,
|
|
1372
|
+
label: string,
|
|
1373
|
+
): ResolvedAccountContextOrganization {
|
|
1374
|
+
if (!organization || typeof organization !== 'object') {
|
|
1375
|
+
throw new TypeError(`${label} must be an object`)
|
|
1376
|
+
}
|
|
1377
|
+
if (!['enterprise', 'team'].includes(organization.type)) {
|
|
1378
|
+
throw new TypeError(`${label} type is invalid`)
|
|
1379
|
+
}
|
|
1380
|
+
if (organization.type === 'enterprise' && !organization.verificationStatus) {
|
|
1381
|
+
throw new TypeError(`${label} enterprise verificationStatus is required`)
|
|
1382
|
+
}
|
|
1383
|
+
if (
|
|
1384
|
+
organization.verificationStatus &&
|
|
1385
|
+
!['unverified', 'pending', 'rejected', 'verified', 'expired'].includes(
|
|
1386
|
+
organization.verificationStatus,
|
|
1387
|
+
)
|
|
1388
|
+
) {
|
|
1389
|
+
throw new TypeError(`${label} verificationStatus is invalid`)
|
|
1390
|
+
}
|
|
1391
|
+
const name = normalizeRequiredText(organization.name, 'Organization name', 100)
|
|
1392
|
+
const employeeDisplayName = normalizeOptionalText(organization.employeeDisplayName, 80)
|
|
1393
|
+
const employeeAvatarText = normalizeOptionalText(organization.employeeAvatarText, 4)
|
|
1394
|
+
const employeeAvatarUrl = normalizeSafeImageUrl(
|
|
1395
|
+
organization.employeeAvatarUrl,
|
|
1396
|
+
'account-context employee avatar URL',
|
|
1397
|
+
)
|
|
1398
|
+
if (!employeeDisplayName && (employeeAvatarText || employeeAvatarUrl)) {
|
|
1399
|
+
throw new TypeError(`${label} employeeDisplayName is required for an employee avatar`)
|
|
1400
|
+
}
|
|
1401
|
+
return Object.freeze({
|
|
1402
|
+
id: normalizeRequiredText(organization.id, 'Organization id', 128),
|
|
1403
|
+
name,
|
|
1404
|
+
type: organization.type,
|
|
1405
|
+
avatarText: normalizeOptionalText(organization.avatarText, 4) || Array.from(name)[0] || '组',
|
|
1406
|
+
employeeDisplayName,
|
|
1407
|
+
employeeAvatarText:
|
|
1408
|
+
employeeAvatarText || (employeeDisplayName ? Array.from(employeeDisplayName)[0] || '员' : ''),
|
|
1409
|
+
employeeAvatarUrl,
|
|
1410
|
+
companyName: normalizeOptionalText(organization.companyName, 160),
|
|
1411
|
+
employeeDepartmentName: normalizeOptionalText(organization.employeeDepartmentName, 100),
|
|
1412
|
+
employeeEmploymentStatus: normalizeOptionalText(organization.employeeEmploymentStatus, 32),
|
|
1413
|
+
verificationStatus:
|
|
1414
|
+
organization.type === 'enterprise' ? (organization.verificationStatus ?? null) : null,
|
|
1415
|
+
role: normalizeOptionalText(organization.role, 80),
|
|
1416
|
+
memberCount: normalizeMemberCount(organization.memberCount),
|
|
1417
|
+
})
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function normalizeOrganizationCount(
|
|
1421
|
+
value: unknown,
|
|
1422
|
+
organization: ResolvedAccountContextOrganization | null,
|
|
1423
|
+
providedCount: number,
|
|
1424
|
+
): number {
|
|
1425
|
+
const fallback = providedCount || (organization ? 1 : 0)
|
|
1426
|
+
if (value === undefined) return fallback
|
|
1427
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value !== fallback) {
|
|
1428
|
+
throw new TypeError(
|
|
1429
|
+
'account-context organizationCount must equal the projected organization count',
|
|
1430
|
+
)
|
|
1431
|
+
}
|
|
1432
|
+
return value
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function normalizeMemberCount(value: unknown): number | null {
|
|
1436
|
+
if (value === undefined || value === null) return null
|
|
1437
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
1438
|
+
throw new TypeError('account-context memberCount must be a non-negative integer')
|
|
1439
|
+
}
|
|
1440
|
+
return value
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function normalizeEmployeeInfo(value: unknown): AccountContextEmployeeInfo {
|
|
1444
|
+
if (!value || typeof value !== 'object') {
|
|
1445
|
+
throw new TypeError('account-context employee info must be an object')
|
|
1446
|
+
}
|
|
1447
|
+
const source = value as { departmentName?: unknown }
|
|
1448
|
+
return Object.freeze({
|
|
1449
|
+
departmentName: normalizeOptionalText(source.departmentName, 100) || null,
|
|
1450
|
+
})
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
function normalizeMaskedMobile(value: unknown): string {
|
|
1454
|
+
const mobile = normalizeOptionalText(value, 40)
|
|
1455
|
+
if (/^1[3-9]\d{9}$/.test(mobile)) {
|
|
1456
|
+
throw new TypeError('account-context only accepts a masked mobile number')
|
|
1457
|
+
}
|
|
1458
|
+
return mobile
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function normalizeRequiredText(value: unknown, label: string, maxLength: number): string {
|
|
1462
|
+
const normalized = normalizeOptionalText(value, maxLength)
|
|
1463
|
+
if (!normalized) throw new TypeError(`${label} is required`)
|
|
1464
|
+
return normalized
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function normalizeOptionalText(value: unknown, maxLength: number): string {
|
|
1468
|
+
if (value === undefined || value === null) return ''
|
|
1469
|
+
if (typeof value !== 'string') throw new TypeError('account-context text values must be strings')
|
|
1470
|
+
const normalized = value.trim()
|
|
1471
|
+
if (normalized.length > maxLength) throw new TypeError('account-context text value is too long')
|
|
1472
|
+
return normalized
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function organizationStatus(
|
|
1476
|
+
organization: ResolvedAccountContextOrganization,
|
|
1477
|
+
): EnterpriseVerificationStatus | 'team' {
|
|
1478
|
+
return organization.type === 'team' ? 'team' : (organization.verificationStatus ?? 'unverified')
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function organizationStatusDescription(status: EnterpriseVerificationStatus | 'team'): string {
|
|
1482
|
+
if (status === 'team') return '团队组织,无需认证'
|
|
1483
|
+
if (status === 'verified') return '企业已认证'
|
|
1484
|
+
if (status === 'pending') return '企业认证审核中'
|
|
1485
|
+
if (status === 'rejected') return '企业认证未通过'
|
|
1486
|
+
if (status === 'expired') return '企业认证已过期'
|
|
1487
|
+
return '企业未认证'
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function organizationStatusIcon(status: EnterpriseVerificationStatus | 'team'): PrototypeIconName {
|
|
1491
|
+
if (status === 'team') return 'users'
|
|
1492
|
+
if (status === 'verified') return 'discount-check-filled'
|
|
1493
|
+
if (status === 'pending') return 'clock-hour-4'
|
|
1494
|
+
if (status === 'rejected') return 'alert-triangle'
|
|
1495
|
+
if (status === 'expired') return 'clock-exclamation'
|
|
1496
|
+
return 'alert-circle'
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function employeeEmploymentStatus(value: string): { className: string; label: string } {
|
|
1500
|
+
if (value === 'inactive' || value === '离职') return { className: 'inactive', label: '非在职' }
|
|
1501
|
+
return { className: 'active', label: '在职' }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function stopEventPropagation(event: Event): void {
|
|
1505
|
+
event.stopPropagation()
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function openPendingAccountSettingsWindow(): Window | null {
|
|
1509
|
+
if (typeof window === 'undefined') return null
|
|
1510
|
+
const accountSettingsWindow = window.open('about:blank', '_blank')
|
|
1511
|
+
if (accountSettingsWindow) accountSettingsWindow.opener = null
|
|
1512
|
+
return accountSettingsWindow
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
function closePendingAccountSettingsWindow(accountSettingsWindow: Window | null): void {
|
|
1516
|
+
if (!accountSettingsWindow || accountSettingsWindow.closed) return
|
|
1517
|
+
accountSettingsWindow.close()
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
function navigateToAccountSettings(url: string, accountSettingsWindow: Window | null): void {
|
|
1521
|
+
if (typeof window === 'undefined') throw new Error('Account settings navigation is unavailable')
|
|
1522
|
+
const target = new URL(url, window.location.href)
|
|
1523
|
+
const ticket = new URLSearchParams(target.hash.slice(1)).get(
|
|
1524
|
+
UNIPLAT_APPLICATION_TICKET_FRAGMENT_KEY,
|
|
1525
|
+
)
|
|
1526
|
+
if (
|
|
1527
|
+
!['http:', 'https:'].includes(target.protocol) ||
|
|
1528
|
+
target.username ||
|
|
1529
|
+
target.password ||
|
|
1530
|
+
!ticket
|
|
1531
|
+
) {
|
|
1532
|
+
throw new Error('Account settings navigation is invalid')
|
|
1533
|
+
}
|
|
1534
|
+
if (accountSettingsWindow && !accountSettingsWindow.closed) {
|
|
1535
|
+
accountSettingsWindow.location.replace(target.href)
|
|
1536
|
+
return
|
|
1537
|
+
}
|
|
1538
|
+
const openedWindow = window.open(target.href, '_blank')
|
|
1539
|
+
if (openedWindow) openedWindow.opener = null
|
|
1540
|
+
}
|