@csntgao/uni-base 0.1.2 → 0.1.4
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 +12 -8
- package/CLAUDE.md +23 -12
- package/README.md +29 -5
- 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 +3 -3
- package/examples/vanilla/main.js +25 -6
- package/package.json +1 -1
- package/packages/web-components/package.json +1 -1
- package/packages/web-components/scripts/verify-runtime-build.mjs +1 -0
- package/packages/web-components/src/account-context.ts +3 -1
- package/packages/web-components/src/index.ts +5 -0
- package/packages/web-components/src/organization-selector.ts +435 -39
- package/packages/web-components/src/user-login.ts +13 -2
- package/packages/web-components/tests/account-context.test.ts +33 -0
- package/packages/web-components/tests/organization-selector.test.ts +226 -14
- package/packages/web-components/tests/user-login.test.ts +27 -0
- package//344/272/244/346/216/245/346/226/207/346/241/243.md +297 -157
|
@@ -3,6 +3,8 @@ import { LitElement, css, html, nothing, type TemplateResult } from 'lit'
|
|
|
3
3
|
export type OrganizationSelectorType = 'enterprise' | 'team'
|
|
4
4
|
export type EnterpriseVerificationStatus =
|
|
5
5
|
'unverified' | 'pending' | 'rejected' | 'verified' | 'expired'
|
|
6
|
+
export type OrganizationSelectorPresentation = 'inline' | 'modal'
|
|
7
|
+
export type OrganizationSelectorTypeFilter = 'all' | OrganizationSelectorType
|
|
6
8
|
|
|
7
9
|
export interface OrganizationSelectorEntry {
|
|
8
10
|
id: string
|
|
@@ -12,12 +14,16 @@ export interface OrganizationSelectorEntry {
|
|
|
12
14
|
role?: string
|
|
13
15
|
memberCount?: number
|
|
14
16
|
lastEnteredLabel?: string
|
|
17
|
+
unifiedSocialCreditCode?: string
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export interface OrganizationSelectorConfig {
|
|
18
21
|
organizations: readonly OrganizationSelectorEntry[]
|
|
19
22
|
selectedOrganizationId?: string
|
|
20
23
|
rememberSelection?: boolean
|
|
24
|
+
displayName?: string
|
|
25
|
+
maskedMobile?: string
|
|
26
|
+
presentation?: OrganizationSelectorPresentation
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
export interface OrganizationSelectorEventMap {
|
|
@@ -26,6 +32,17 @@ export interface OrganizationSelectorEventMap {
|
|
|
26
32
|
rememberSelection: boolean
|
|
27
33
|
}>
|
|
28
34
|
'organization-create-requested': CustomEvent<Record<string, never>>
|
|
35
|
+
'logout-requested': CustomEvent<Record<string, never>>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type OrganizationEntryDecision =
|
|
39
|
+
| { kind: 'personal' }
|
|
40
|
+
| { kind: 'auto-enter'; organizationId: string }
|
|
41
|
+
| { kind: 'prompt'; selectedOrganizationId: string }
|
|
42
|
+
|
|
43
|
+
export interface OrganizationEntryDecisionInput {
|
|
44
|
+
organizations: readonly OrganizationSelectorEntry[]
|
|
45
|
+
rememberedOrganizationId?: string
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
interface ResolvedOrganizationSelectorEntry {
|
|
@@ -36,6 +53,7 @@ interface ResolvedOrganizationSelectorEntry {
|
|
|
36
53
|
role: string
|
|
37
54
|
memberCount: number | null
|
|
38
55
|
lastEnteredLabel: string
|
|
56
|
+
searchKey: string
|
|
39
57
|
}
|
|
40
58
|
|
|
41
59
|
export class UniplatOrganizationSelector extends LitElement {
|
|
@@ -43,15 +61,22 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
43
61
|
organizations: { state: true },
|
|
44
62
|
selectedOrganizationId: { state: true },
|
|
45
63
|
rememberSelection: { state: true },
|
|
64
|
+
displayName: { state: true },
|
|
65
|
+
maskedMobile: { state: true },
|
|
66
|
+
presentation: { state: true },
|
|
67
|
+
searchQuery: { state: true },
|
|
68
|
+
typeFilter: { state: true },
|
|
46
69
|
}
|
|
47
70
|
|
|
48
71
|
static override styles = css`
|
|
49
72
|
:host {
|
|
50
|
-
--uniplat-organization-selector-width:
|
|
73
|
+
--uniplat-organization-selector-width: 420px;
|
|
51
74
|
--uniplat-organization-selector-primary: #2e6bf5;
|
|
52
75
|
--uniplat-organization-selector-text: #182634;
|
|
53
76
|
--uniplat-organization-selector-muted: #7a8794;
|
|
54
77
|
--uniplat-organization-selector-border: #e4ebf3;
|
|
78
|
+
--uniplat-organization-selector-overlay: rgba(24, 38, 52, 0.55);
|
|
79
|
+
--uniplat-organization-selector-modal-z-index: 1200;
|
|
55
80
|
--uniplat-organization-selector-font-family:
|
|
56
81
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
57
82
|
display: block;
|
|
@@ -61,6 +86,13 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
61
86
|
line-height: 1.5;
|
|
62
87
|
}
|
|
63
88
|
|
|
89
|
+
:host([data-presentation='modal']) {
|
|
90
|
+
position: fixed;
|
|
91
|
+
z-index: var(--uniplat-organization-selector-modal-z-index);
|
|
92
|
+
inset: 0;
|
|
93
|
+
width: auto;
|
|
94
|
+
}
|
|
95
|
+
|
|
64
96
|
* {
|
|
65
97
|
box-sizing: border-box;
|
|
66
98
|
}
|
|
@@ -74,6 +106,20 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
74
106
|
cursor: pointer;
|
|
75
107
|
}
|
|
76
108
|
|
|
109
|
+
.overlay {
|
|
110
|
+
display: grid;
|
|
111
|
+
width: 100%;
|
|
112
|
+
height: 100%;
|
|
113
|
+
padding: 24px;
|
|
114
|
+
overflow: auto;
|
|
115
|
+
background: var(--uniplat-organization-selector-overlay);
|
|
116
|
+
place-items: center;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.overlay .card {
|
|
120
|
+
width: min(var(--uniplat-organization-selector-width), 100%);
|
|
121
|
+
}
|
|
122
|
+
|
|
77
123
|
.card {
|
|
78
124
|
width: 100%;
|
|
79
125
|
padding: 24px;
|
|
@@ -89,27 +135,105 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
89
135
|
margin: 0;
|
|
90
136
|
}
|
|
91
137
|
|
|
138
|
+
.header {
|
|
139
|
+
display: flex;
|
|
140
|
+
align-items: baseline;
|
|
141
|
+
justify-content: space-between;
|
|
142
|
+
gap: 12px;
|
|
143
|
+
}
|
|
144
|
+
|
|
92
145
|
h2 {
|
|
93
|
-
font-size:
|
|
146
|
+
font-size: 19px;
|
|
94
147
|
font-weight: 800;
|
|
95
148
|
line-height: 1.35;
|
|
96
149
|
}
|
|
97
150
|
|
|
151
|
+
.identity {
|
|
152
|
+
flex: none;
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
color: var(--uniplat-organization-selector-muted);
|
|
155
|
+
font-size: 12px;
|
|
156
|
+
text-overflow: ellipsis;
|
|
157
|
+
white-space: nowrap;
|
|
158
|
+
}
|
|
159
|
+
|
|
98
160
|
.description {
|
|
99
161
|
margin-top: 7px;
|
|
100
162
|
color: var(--uniplat-organization-selector-muted);
|
|
163
|
+
font-size: 12.5px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.search {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 8px;
|
|
170
|
+
margin-top: 15px;
|
|
171
|
+
padding: 0 12px;
|
|
172
|
+
border: 1px solid #e9eef5;
|
|
173
|
+
border-radius: 10px;
|
|
174
|
+
background: #f7f9fc;
|
|
175
|
+
color: #98a4b2;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.search input {
|
|
179
|
+
flex: 1;
|
|
180
|
+
min-width: 0;
|
|
181
|
+
min-height: 40px;
|
|
182
|
+
padding: 0;
|
|
183
|
+
border: 0;
|
|
184
|
+
color: var(--uniplat-organization-selector-text);
|
|
185
|
+
background: transparent;
|
|
101
186
|
font-size: 13px;
|
|
187
|
+
outline: none;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.search svg {
|
|
191
|
+
width: 16px;
|
|
192
|
+
height: 16px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.filters {
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-wrap: wrap;
|
|
198
|
+
gap: 8px;
|
|
199
|
+
margin-top: 11px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.chip {
|
|
203
|
+
padding: 4px 11px;
|
|
204
|
+
border: 1px solid #e9eef5;
|
|
205
|
+
border-radius: 999px;
|
|
206
|
+
color: #6b7885;
|
|
207
|
+
background: #f4f7fb;
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
font-weight: 650;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.chip.selected {
|
|
213
|
+
border-color: var(--uniplat-organization-selector-primary);
|
|
214
|
+
color: #ffffff;
|
|
215
|
+
background: var(--uniplat-organization-selector-primary);
|
|
102
216
|
}
|
|
103
217
|
|
|
104
218
|
.list {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
max-height: 340px;
|
|
108
|
-
margin-top: 18px;
|
|
219
|
+
max-height: 392px;
|
|
220
|
+
margin-top: 4px;
|
|
109
221
|
overflow-y: auto;
|
|
110
222
|
scrollbar-gutter: stable;
|
|
111
223
|
}
|
|
112
224
|
|
|
225
|
+
.group-label {
|
|
226
|
+
padding: 14px 0 8px;
|
|
227
|
+
color: #98a4b2;
|
|
228
|
+
font-size: 11.5px;
|
|
229
|
+
font-weight: 700;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.group {
|
|
233
|
+
display: grid;
|
|
234
|
+
gap: 9px;
|
|
235
|
+
}
|
|
236
|
+
|
|
113
237
|
.organization {
|
|
114
238
|
display: grid;
|
|
115
239
|
grid-template-columns: 36px minmax(0, 1fr) 18px;
|
|
@@ -205,18 +329,27 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
205
329
|
white-space: nowrap;
|
|
206
330
|
}
|
|
207
331
|
|
|
208
|
-
.
|
|
332
|
+
.indicator {
|
|
209
333
|
display: grid;
|
|
210
334
|
place-items: center;
|
|
211
335
|
width: 18px;
|
|
212
336
|
height: 18px;
|
|
213
|
-
border: 1.5px solid #c7d1df;
|
|
214
|
-
border-radius: 50%;
|
|
215
337
|
color: transparent;
|
|
216
338
|
}
|
|
217
339
|
|
|
218
|
-
.
|
|
219
|
-
|
|
340
|
+
.indicator svg {
|
|
341
|
+
width: 16px;
|
|
342
|
+
height: 16px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.organization:hover .indicator {
|
|
346
|
+
color: #c7d1df;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.organization.selected .indicator {
|
|
350
|
+
width: 18px;
|
|
351
|
+
height: 18px;
|
|
352
|
+
border-radius: 50%;
|
|
220
353
|
color: #ffffff;
|
|
221
354
|
background: var(--uniplat-organization-selector-primary);
|
|
222
355
|
}
|
|
@@ -254,21 +387,48 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
254
387
|
font-weight: 650;
|
|
255
388
|
}
|
|
256
389
|
|
|
390
|
+
.actions {
|
|
391
|
+
display: flex;
|
|
392
|
+
align-items: stretch;
|
|
393
|
+
gap: 10px;
|
|
394
|
+
margin-top: 13px;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.logout,
|
|
257
398
|
.enter {
|
|
258
399
|
display: flex;
|
|
259
400
|
align-items: center;
|
|
260
401
|
justify-content: center;
|
|
261
|
-
width: 100%;
|
|
262
402
|
min-height: 46px;
|
|
263
|
-
|
|
264
|
-
border: 0;
|
|
403
|
+
padding: 0 14px;
|
|
265
404
|
border-radius: 10px;
|
|
266
|
-
color: #ffffff;
|
|
267
|
-
background: var(--uniplat-organization-selector-primary);
|
|
268
405
|
font-size: 14px;
|
|
269
406
|
font-weight: 700;
|
|
270
407
|
}
|
|
271
408
|
|
|
409
|
+
.logout {
|
|
410
|
+
flex: none;
|
|
411
|
+
min-width: 104px;
|
|
412
|
+
border: 1px solid #dbe3ed;
|
|
413
|
+
color: #56636f;
|
|
414
|
+
background: #ffffff;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.logout:hover {
|
|
418
|
+
background: #f7f9fc;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.enter {
|
|
422
|
+
flex: 1;
|
|
423
|
+
min-width: 0;
|
|
424
|
+
overflow: hidden;
|
|
425
|
+
border: 0;
|
|
426
|
+
color: #ffffff;
|
|
427
|
+
background: var(--uniplat-organization-selector-primary);
|
|
428
|
+
text-overflow: ellipsis;
|
|
429
|
+
white-space: nowrap;
|
|
430
|
+
}
|
|
431
|
+
|
|
272
432
|
.enter:disabled {
|
|
273
433
|
cursor: not-allowed;
|
|
274
434
|
background: #a9bff6;
|
|
@@ -278,7 +438,7 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
278
438
|
display: grid;
|
|
279
439
|
min-height: 120px;
|
|
280
440
|
place-items: center;
|
|
281
|
-
margin-top:
|
|
441
|
+
margin-top: 14px;
|
|
282
442
|
border: 1px dashed #dce5f0;
|
|
283
443
|
border-radius: 10px;
|
|
284
444
|
color: #8b98a6;
|
|
@@ -300,14 +460,25 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
300
460
|
declare private organizations: readonly ResolvedOrganizationSelectorEntry[]
|
|
301
461
|
declare private selectedOrganizationId: string
|
|
302
462
|
declare private rememberSelection: boolean
|
|
463
|
+
declare private displayName: string
|
|
464
|
+
declare private maskedMobile: string
|
|
465
|
+
declare private presentation: OrganizationSelectorPresentation
|
|
466
|
+
declare private searchQuery: string
|
|
467
|
+
declare private typeFilter: OrganizationSelectorTypeFilter
|
|
303
468
|
|
|
304
469
|
private configured = false
|
|
470
|
+
private escapeGuardTarget: Document | null = null
|
|
305
471
|
|
|
306
472
|
constructor() {
|
|
307
473
|
super()
|
|
308
474
|
this.organizations = []
|
|
309
475
|
this.selectedOrganizationId = ''
|
|
310
476
|
this.rememberSelection = true
|
|
477
|
+
this.displayName = ''
|
|
478
|
+
this.maskedMobile = ''
|
|
479
|
+
this.presentation = 'inline'
|
|
480
|
+
this.searchQuery = ''
|
|
481
|
+
this.typeFilter = 'all'
|
|
311
482
|
}
|
|
312
483
|
|
|
313
484
|
configure(config: OrganizationSelectorConfig): void {
|
|
@@ -319,26 +490,87 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
319
490
|
this.organizations = organizations
|
|
320
491
|
this.selectedOrganizationId = organizations.some(({ id }) => id === requestedSelection)
|
|
321
492
|
? requestedSelection
|
|
322
|
-
: (organizations
|
|
493
|
+
: defaultSelectedOrganizationId(organizations)
|
|
323
494
|
this.rememberSelection = config.rememberSelection ?? true
|
|
495
|
+
this.displayName = normalizeOptionalText(config.displayName, 60)
|
|
496
|
+
this.maskedMobile = normalizeMaskedMobile(config.maskedMobile)
|
|
497
|
+
this.presentation = normalizePresentation(config.presentation)
|
|
498
|
+
this.searchQuery = ''
|
|
499
|
+
this.typeFilter = 'all'
|
|
324
500
|
this.configured = true
|
|
501
|
+
this.syncPresentation()
|
|
325
502
|
this.requestUpdate()
|
|
326
503
|
}
|
|
327
504
|
|
|
505
|
+
override connectedCallback(): void {
|
|
506
|
+
super.connectedCallback()
|
|
507
|
+
this.syncPresentation()
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
override disconnectedCallback(): void {
|
|
511
|
+
this.releaseEscapeGuard()
|
|
512
|
+
super.disconnectedCallback()
|
|
513
|
+
}
|
|
514
|
+
|
|
328
515
|
override render() {
|
|
329
516
|
if (!this.configured) return nothing
|
|
517
|
+
if (this.presentation === 'modal') {
|
|
518
|
+
return html`<div class="overlay" part="overlay" role="presentation">
|
|
519
|
+
${this.renderCard(true)}
|
|
520
|
+
</div>`
|
|
521
|
+
}
|
|
522
|
+
return this.renderCard(false)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private renderCard(modal: boolean) {
|
|
526
|
+
const visible = this.visibleOrganizations()
|
|
527
|
+
const recent = visible.filter((organization) => organization.lastEnteredLabel !== '')
|
|
528
|
+
const others = visible.filter((organization) => organization.lastEnteredLabel === '')
|
|
529
|
+
const selected = this.organizations.find(({ id }) => id === this.selectedOrganizationId)
|
|
330
530
|
return html`
|
|
331
|
-
<section
|
|
332
|
-
|
|
531
|
+
<section
|
|
532
|
+
class="card"
|
|
533
|
+
part="shell"
|
|
534
|
+
role=${modal ? 'dialog' : 'group'}
|
|
535
|
+
aria-modal=${modal ? 'true' : nothing}
|
|
536
|
+
aria-label="选择团队或企业"
|
|
537
|
+
>
|
|
538
|
+
<div class="header">
|
|
539
|
+
<h2 part="title">选择要进入的团队 / 企业</h2>
|
|
540
|
+
${
|
|
541
|
+
this.identityLabel()
|
|
542
|
+
? html`<span class="identity" part="identity">${this.identityLabel()}</span>`
|
|
543
|
+
: nothing
|
|
544
|
+
}
|
|
545
|
+
</div>
|
|
333
546
|
<p class="description" part="description">
|
|
334
|
-
|
|
547
|
+
你共归属 ${this.organizations.length} 个组织,进入后可在导航随时切换
|
|
335
548
|
</p>
|
|
549
|
+
${this.organizations.length ? this.renderSearch() : nothing}
|
|
550
|
+
${this.organizations.length ? this.renderFilters() : nothing}
|
|
336
551
|
${
|
|
337
|
-
this.organizations.length
|
|
338
|
-
? html`<div class="
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
552
|
+
!this.organizations.length
|
|
553
|
+
? html`<div class="empty" part="empty">暂无可进入的团队或企业</div>`
|
|
554
|
+
: visible.length
|
|
555
|
+
? html`<div class="list" part="organization-list">
|
|
556
|
+
${
|
|
557
|
+
recent.length
|
|
558
|
+
? html`<div class="group-label" part="group-label">最近进入</div>
|
|
559
|
+
<div class="group">
|
|
560
|
+
${recent.map((organization) => this.renderOrganization(organization))}
|
|
561
|
+
</div>`
|
|
562
|
+
: nothing
|
|
563
|
+
}
|
|
564
|
+
${
|
|
565
|
+
others.length
|
|
566
|
+
? html`<div class="group-label" part="group-label">全部组织</div>
|
|
567
|
+
<div class="group">
|
|
568
|
+
${others.map((organization) => this.renderOrganization(organization))}
|
|
569
|
+
</div>`
|
|
570
|
+
: nothing
|
|
571
|
+
}
|
|
572
|
+
</div>`
|
|
573
|
+
: html`<div class="empty" part="empty">没有匹配的团队或企业</div>`
|
|
342
574
|
}
|
|
343
575
|
<div class="footer-row">
|
|
344
576
|
<label class="remember" part="remember-selection">
|
|
@@ -352,22 +584,74 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
352
584
|
记住选择,下次直接进入
|
|
353
585
|
</label>
|
|
354
586
|
<button class="create" part="create-action" type="button" @click=${this.requestCreate}>
|
|
355
|
-
+ 创建
|
|
587
|
+
+ 创建 / 加入
|
|
588
|
+
</button>
|
|
589
|
+
</div>
|
|
590
|
+
<div class="actions">
|
|
591
|
+
<button class="logout" part="logout-action" type="button" @click=${this.requestLogout}>
|
|
592
|
+
退出登录
|
|
593
|
+
</button>
|
|
594
|
+
<button
|
|
595
|
+
class="enter"
|
|
596
|
+
part="enter-action"
|
|
597
|
+
type="button"
|
|
598
|
+
?disabled=${!selected}
|
|
599
|
+
@click=${this.requestEnter}
|
|
600
|
+
>
|
|
601
|
+
${selected ? `进入「${selected.name}」` : '进入'}
|
|
356
602
|
</button>
|
|
357
603
|
</div>
|
|
358
|
-
<button
|
|
359
|
-
class="enter"
|
|
360
|
-
part="enter-action"
|
|
361
|
-
type="button"
|
|
362
|
-
?disabled=${!this.selectedOrganizationId}
|
|
363
|
-
@click=${this.requestEnter}
|
|
364
|
-
>
|
|
365
|
-
进入
|
|
366
|
-
</button>
|
|
367
604
|
</section>
|
|
368
605
|
`
|
|
369
606
|
}
|
|
370
607
|
|
|
608
|
+
private renderSearch() {
|
|
609
|
+
return html`
|
|
610
|
+
<div class="search" part="search">
|
|
611
|
+
${icon('search')}
|
|
612
|
+
<input
|
|
613
|
+
type="search"
|
|
614
|
+
part="search-input"
|
|
615
|
+
placeholder="搜索名称,或输入统一社会信用代码"
|
|
616
|
+
aria-label="搜索团队或企业"
|
|
617
|
+
.value=${this.searchQuery}
|
|
618
|
+
@input=${(event: Event) => {
|
|
619
|
+
this.searchQuery = (event.currentTarget as HTMLInputElement).value
|
|
620
|
+
}}
|
|
621
|
+
/>
|
|
622
|
+
</div>
|
|
623
|
+
`
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private renderFilters() {
|
|
627
|
+
const enterprises = this.organizations.filter(({ type }) => type === 'enterprise').length
|
|
628
|
+
const teams = this.organizations.length - enterprises
|
|
629
|
+
const filters: readonly { value: OrganizationSelectorTypeFilter; label: string }[] = [
|
|
630
|
+
{ value: 'all', label: `全部 ${this.organizations.length}` },
|
|
631
|
+
{ value: 'enterprise', label: `企业 ${enterprises}` },
|
|
632
|
+
{ value: 'team', label: `团队 ${teams}` },
|
|
633
|
+
]
|
|
634
|
+
return html`
|
|
635
|
+
<div class="filters" part="type-filters">
|
|
636
|
+
${filters.map(
|
|
637
|
+
(filter) => html`
|
|
638
|
+
<button
|
|
639
|
+
class="chip ${this.typeFilter === filter.value ? 'selected' : ''}"
|
|
640
|
+
part="type-filter"
|
|
641
|
+
type="button"
|
|
642
|
+
aria-pressed=${String(this.typeFilter === filter.value)}
|
|
643
|
+
@click=${() => {
|
|
644
|
+
this.typeFilter = filter.value
|
|
645
|
+
}}
|
|
646
|
+
>
|
|
647
|
+
${filter.label}
|
|
648
|
+
</button>
|
|
649
|
+
`,
|
|
650
|
+
)}
|
|
651
|
+
</div>
|
|
652
|
+
`
|
|
653
|
+
}
|
|
654
|
+
|
|
371
655
|
private renderOrganization(organization: ResolvedOrganizationSelectorEntry) {
|
|
372
656
|
const selected = organization.id === this.selectedOrganizationId
|
|
373
657
|
const badge = organizationBadge(organization)
|
|
@@ -397,11 +681,55 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
397
681
|
</span>
|
|
398
682
|
<span class="meta">${organizationMeta(organization)}</span>
|
|
399
683
|
</span>
|
|
400
|
-
<span class="
|
|
684
|
+
<span class="indicator">${icon(selected ? 'check' : 'chevron')}</span>
|
|
401
685
|
</button>
|
|
402
686
|
`
|
|
403
687
|
}
|
|
404
688
|
|
|
689
|
+
private identityLabel(): string {
|
|
690
|
+
return [this.displayName, this.maskedMobile].filter((value) => value !== '').join(' · ')
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private visibleOrganizations(): readonly ResolvedOrganizationSelectorEntry[] {
|
|
694
|
+
const query = this.searchQuery.trim().toLowerCase()
|
|
695
|
+
return this.organizations.filter((organization) => {
|
|
696
|
+
if (this.typeFilter !== 'all' && organization.type !== this.typeFilter) return false
|
|
697
|
+
if (!query) return true
|
|
698
|
+
return organization.searchKey.includes(query)
|
|
699
|
+
})
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private syncPresentation(): void {
|
|
703
|
+
if (this.configured && this.presentation === 'modal') {
|
|
704
|
+
this.setAttribute('data-presentation', 'modal')
|
|
705
|
+
this.captureEscapeGuard()
|
|
706
|
+
return
|
|
707
|
+
}
|
|
708
|
+
this.removeAttribute('data-presentation')
|
|
709
|
+
this.releaseEscapeGuard()
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private captureEscapeGuard(): void {
|
|
713
|
+
if (!this.isConnected) return
|
|
714
|
+
const target = this.ownerDocument
|
|
715
|
+
if (this.escapeGuardTarget === target) return
|
|
716
|
+
this.releaseEscapeGuard()
|
|
717
|
+
target.addEventListener('keydown', this.guardEscape, true)
|
|
718
|
+
this.escapeGuardTarget = target
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private releaseEscapeGuard(): void {
|
|
722
|
+
this.escapeGuardTarget?.removeEventListener('keydown', this.guardEscape, true)
|
|
723
|
+
this.escapeGuardTarget = null
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 强制态:Escape 不关闭选择弹窗,也不允许冒泡到宿主的全局关闭逻辑。
|
|
727
|
+
private readonly guardEscape = (event: KeyboardEvent) => {
|
|
728
|
+
if (event.key !== 'Escape') return
|
|
729
|
+
event.preventDefault()
|
|
730
|
+
event.stopPropagation()
|
|
731
|
+
}
|
|
732
|
+
|
|
405
733
|
private readonly requestCreate = () => {
|
|
406
734
|
this.dispatchEvent(
|
|
407
735
|
new CustomEvent('organization-create-requested', {
|
|
@@ -412,6 +740,16 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
412
740
|
)
|
|
413
741
|
}
|
|
414
742
|
|
|
743
|
+
private readonly requestLogout = () => {
|
|
744
|
+
this.dispatchEvent(
|
|
745
|
+
new CustomEvent('logout-requested', {
|
|
746
|
+
detail: {},
|
|
747
|
+
bubbles: true,
|
|
748
|
+
composed: true,
|
|
749
|
+
}),
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
|
|
415
753
|
private readonly requestEnter = () => {
|
|
416
754
|
if (!this.selectedOrganizationId) return
|
|
417
755
|
this.dispatchEvent(
|
|
@@ -427,6 +765,34 @@ export class UniplatOrganizationSelector extends LitElement {
|
|
|
427
765
|
}
|
|
428
766
|
}
|
|
429
767
|
|
|
768
|
+
/**
|
|
769
|
+
* S.0 登录后的入口判定:0 个归属走个人态,仅 1 个归属或命中「记住选择」直接进入,
|
|
770
|
+
* 2 个及以上且未记住时才需要展示选择弹窗。函数无副作用,不读写宿主存储。
|
|
771
|
+
*/
|
|
772
|
+
export function resolveOrganizationEntryDecision(
|
|
773
|
+
input: OrganizationEntryDecisionInput,
|
|
774
|
+
): OrganizationEntryDecision {
|
|
775
|
+
if (!input || !Array.isArray(input.organizations)) {
|
|
776
|
+
throw new TypeError('organization entry decision requires an organizations array')
|
|
777
|
+
}
|
|
778
|
+
const organizations = normalizeOrganizations(input.organizations)
|
|
779
|
+
const first = organizations[0]
|
|
780
|
+
if (!first) return { kind: 'personal' }
|
|
781
|
+
if (organizations.length === 1) return { kind: 'auto-enter', organizationId: first.id }
|
|
782
|
+
const remembered = normalizeOptionalText(input.rememberedOrganizationId, 128)
|
|
783
|
+
if (remembered && organizations.some(({ id }) => id === remembered)) {
|
|
784
|
+
return { kind: 'auto-enter', organizationId: remembered }
|
|
785
|
+
}
|
|
786
|
+
return { kind: 'prompt', selectedOrganizationId: defaultSelectedOrganizationId(organizations) }
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function defaultSelectedOrganizationId(
|
|
790
|
+
organizations: readonly ResolvedOrganizationSelectorEntry[],
|
|
791
|
+
): string {
|
|
792
|
+
const recent = organizations.find((organization) => organization.lastEnteredLabel !== '')
|
|
793
|
+
return recent?.id ?? organizations[0]?.id ?? ''
|
|
794
|
+
}
|
|
795
|
+
|
|
430
796
|
function normalizeOrganizations(
|
|
431
797
|
organizations: readonly OrganizationSelectorEntry[],
|
|
432
798
|
): readonly ResolvedOrganizationSelectorEntry[] {
|
|
@@ -443,18 +809,39 @@ function normalizeOrganizations(
|
|
|
443
809
|
throw new TypeError('organization-selector organization type is invalid')
|
|
444
810
|
}
|
|
445
811
|
const verificationStatus = normalizeVerificationStatus(type, organization.verificationStatus)
|
|
812
|
+
const name = normalizeRequiredText(organization.name, 'Organization name', 100)
|
|
813
|
+
const unifiedSocialCreditCode = normalizeOptionalText(organization.unifiedSocialCreditCode, 32)
|
|
446
814
|
return {
|
|
447
815
|
id,
|
|
448
|
-
name
|
|
816
|
+
name,
|
|
449
817
|
type,
|
|
450
818
|
verificationStatus,
|
|
451
819
|
role: normalizeOptionalText(organization.role, 60),
|
|
452
820
|
memberCount: normalizeMemberCount(organization.memberCount),
|
|
453
821
|
lastEnteredLabel: normalizeOptionalText(organization.lastEnteredLabel, 80),
|
|
822
|
+
searchKey: `${name}${unifiedSocialCreditCode}`.toLowerCase(),
|
|
454
823
|
}
|
|
455
824
|
})
|
|
456
825
|
}
|
|
457
826
|
|
|
827
|
+
function normalizePresentation(
|
|
828
|
+
value: OrganizationSelectorPresentation | undefined,
|
|
829
|
+
): OrganizationSelectorPresentation {
|
|
830
|
+
if (value === undefined) return 'inline'
|
|
831
|
+
if (value !== 'inline' && value !== 'modal') {
|
|
832
|
+
throw new TypeError('organization-selector presentation must be inline or modal')
|
|
833
|
+
}
|
|
834
|
+
return value
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function normalizeMaskedMobile(value: unknown): string {
|
|
838
|
+
const mobile = normalizeOptionalText(value, 40)
|
|
839
|
+
if (/^1[3-9]\d{9}$/.test(mobile)) {
|
|
840
|
+
throw new TypeError('organization-selector only accepts a masked mobile number')
|
|
841
|
+
}
|
|
842
|
+
return mobile
|
|
843
|
+
}
|
|
844
|
+
|
|
458
845
|
function normalizeVerificationStatus(
|
|
459
846
|
type: OrganizationSelectorType,
|
|
460
847
|
value: EnterpriseVerificationStatus | undefined,
|
|
@@ -517,7 +904,7 @@ function organizationBadge(organization: ResolvedOrganizationSelectorEntry) {
|
|
|
517
904
|
return { label: '企业', className: 'enterprise' }
|
|
518
905
|
}
|
|
519
906
|
|
|
520
|
-
function icon(name: OrganizationSelectorType | 'check'): TemplateResult {
|
|
907
|
+
function icon(name: OrganizationSelectorType | 'check' | 'chevron' | 'search'): TemplateResult {
|
|
521
908
|
if (name === 'enterprise') {
|
|
522
909
|
return html`<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
523
910
|
<path d="M5 21V6l7-3 7 3v15M3 21h18" />
|
|
@@ -530,5 +917,14 @@ function icon(name: OrganizationSelectorType | 'check'): TemplateResult {
|
|
|
530
917
|
<path d="M3.5 20v-2a5.5 5.5 0 0 1 11 0v2M16 5.5a3 3 0 0 1 0 5.5M17 14a5 5 0 0 1 3.5 4.8V20" />
|
|
531
918
|
</svg>`
|
|
532
919
|
}
|
|
920
|
+
if (name === 'chevron') {
|
|
921
|
+
return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 6l6 6l-6 6" /></svg>`
|
|
922
|
+
}
|
|
923
|
+
if (name === 'search') {
|
|
924
|
+
return html`<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
925
|
+
<path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0" />
|
|
926
|
+
<path d="M21 21l-6 -6" />
|
|
927
|
+
</svg>`
|
|
928
|
+
}
|
|
533
929
|
return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
|
|
534
930
|
}
|