@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.
Files changed (90) hide show
  1. package/.prettierignore +4 -0
  2. package/.prettierrc.json +6 -0
  3. package/AGENTS.md +519 -0
  4. package/CLAUDE.md +512 -0
  5. package/README.md +734 -0
  6. 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
  7. 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
  8. 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
  9. 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
  10. package/eslint.config.js +30 -0
  11. package/examples/vanilla/index.html +33 -0
  12. package/examples/vanilla/main.js +247 -0
  13. package/examples/vanilla/package.json +15 -0
  14. package/examples/vanilla/public/runtime-config.js +10 -0
  15. package/examples/vanilla/style.css +99 -0
  16. package/package.json +31 -0
  17. package/packages/core/package.json +30 -0
  18. package/packages/core/src/client.ts +1132 -0
  19. package/packages/core/src/customer-support-client.ts +825 -0
  20. package/packages/core/src/customer-support-types.ts +123 -0
  21. package/packages/core/src/index.ts +79 -0
  22. package/packages/core/src/notification-center-client.ts +462 -0
  23. package/packages/core/src/notification-center-types.ts +154 -0
  24. package/packages/core/src/protocol.ts +237 -0
  25. package/packages/core/src/types.ts +170 -0
  26. package/packages/core/tests/client.test.ts +760 -0
  27. package/packages/core/tests/customer-support-client.test.ts +576 -0
  28. package/packages/core/tests/fake-websocket.ts +55 -0
  29. package/packages/core/tests/notification-center-client.test.ts +467 -0
  30. package/packages/core/tests/protocol.test.ts +124 -0
  31. package/packages/core/tsconfig.json +7 -0
  32. package/packages/core/tsup.config.ts +11 -0
  33. package/packages/web-components/package.json +35 -0
  34. package/packages/web-components/scripts/verify-runtime-build.mjs +111 -0
  35. package/packages/web-components/src/account-context.ts +1540 -0
  36. package/packages/web-components/src/application-switcher-transport.ts +243 -0
  37. package/packages/web-components/src/application-switcher.ts +1103 -0
  38. package/packages/web-components/src/chat.ts +1754 -0
  39. package/packages/web-components/src/customer-support-chat.ts +862 -0
  40. package/packages/web-components/src/customer-support-transport.ts +269 -0
  41. package/packages/web-components/src/index.ts +256 -0
  42. package/packages/web-components/src/notification-center.ts +1066 -0
  43. package/packages/web-components/src/notification-popover.ts +615 -0
  44. package/packages/web-components/src/organization-multi-employee-selector.ts +1564 -0
  45. package/packages/web-components/src/organization-onboarding-transport.ts +54 -0
  46. package/packages/web-components/src/organization-onboarding.ts +1216 -0
  47. package/packages/web-components/src/organization-selector.ts +534 -0
  48. package/packages/web-components/src/organization-single-employee-selector.ts +114 -0
  49. package/packages/web-components/src/prototype-icons.ts +242 -0
  50. package/packages/web-components/src/safe-image-url.ts +29 -0
  51. package/packages/web-components/src/safe-markdown.ts +192 -0
  52. package/packages/web-components/src/uniplat-base-account-context-transport.ts +625 -0
  53. package/packages/web-components/src/uniplat-base-application-switcher-transport.ts +418 -0
  54. package/packages/web-components/src/uniplat-base-application-ticket.ts +254 -0
  55. package/packages/web-components/src/uniplat-base-identity-ticket.ts +202 -0
  56. package/packages/web-components/src/uniplat-base-notification-center-transport.ts +378 -0
  57. package/packages/web-components/src/uniplat-base-organization-multi-employee-selector-transport.ts +394 -0
  58. package/packages/web-components/src/uniplat-base-organization-onboarding-transport.ts +183 -0
  59. package/packages/web-components/src/uniplat-base-organization-single-employee-selector-transport.ts +14 -0
  60. package/packages/web-components/src/user-login-transport.ts +563 -0
  61. package/packages/web-components/src/user-login.ts +2027 -0
  62. package/packages/web-components/src/user-menu.ts +880 -0
  63. package/packages/web-components/tests/account-context.test.ts +798 -0
  64. package/packages/web-components/tests/application-switcher.test.ts +562 -0
  65. package/packages/web-components/tests/chat.test.ts +1130 -0
  66. package/packages/web-components/tests/customer-support-chat.test.ts +428 -0
  67. package/packages/web-components/tests/customer-support-transport.test.ts +79 -0
  68. package/packages/web-components/tests/notification-center-transport.test.ts +271 -0
  69. package/packages/web-components/tests/notification-center.test.ts +429 -0
  70. package/packages/web-components/tests/notification-popover.test.ts +283 -0
  71. package/packages/web-components/tests/organization-multi-employee-selector-transport.test.ts +332 -0
  72. package/packages/web-components/tests/organization-multi-employee-selector.test.ts +225 -0
  73. package/packages/web-components/tests/organization-onboarding.test.ts +260 -0
  74. package/packages/web-components/tests/organization-selector.test.ts +127 -0
  75. package/packages/web-components/tests/organization-single-employee-selector.test.ts +145 -0
  76. package/packages/web-components/tests/uniplat-base-account-context-transport.test.ts +496 -0
  77. package/packages/web-components/tests/uniplat-base-application-switcher-transport.test.ts +598 -0
  78. package/packages/web-components/tests/uniplat-base-application-ticket.test.ts +233 -0
  79. package/packages/web-components/tests/uniplat-base-identity-ticket.test.ts +226 -0
  80. package/packages/web-components/tests/uniplat-base-organization-onboarding-transport.test.ts +227 -0
  81. package/packages/web-components/tests/user-login-transport.test.ts +443 -0
  82. package/packages/web-components/tests/user-login.test.ts +626 -0
  83. package/packages/web-components/tests/user-menu.test.ts +271 -0
  84. package/packages/web-components/tsconfig.json +8 -0
  85. package/packages/web-components/tsup.config.ts +12 -0
  86. package/packages/web-components/tsup.runtime.config.ts +37 -0
  87. package/pnpm-workspace.yaml +6 -0
  88. package/tsconfig.base.json +21 -0
  89. package/vitest.config.ts +20 -0
  90. package//344/272/244/346/216/245/346/226/207/346/241/243.md +254 -0
@@ -0,0 +1,1216 @@
1
+ import { LitElement, css, html, nothing, type TemplateResult } from 'lit'
2
+ import {
3
+ OrganizationOnboardingTransportError,
4
+ type CreatedOnboardingOrganization,
5
+ type OrganizationOnboardingTransport,
6
+ } from './organization-onboarding-transport'
7
+
8
+ export type OrganizationOnboardingStep =
9
+ | 'unassigned'
10
+ | 'method-selection'
11
+ | 'skip-confirmation'
12
+ | 'enterprise-form'
13
+ | 'enterprise-success'
14
+ | 'team-form'
15
+ | 'team-success'
16
+
17
+ export interface OrganizationOnboardingConfig {
18
+ transport: OrganizationOnboardingTransport
19
+ initialStep?: 'unassigned' | 'method-selection'
20
+ }
21
+
22
+ export interface OrganizationOnboardingEventMap {
23
+ 'onboarding-skipped': CustomEvent<{
24
+ source: 'unassigned' | 'method-selection' | 'enterprise-form' | 'team-form'
25
+ }>
26
+ 'join-organization-requested': CustomEvent<{ enterpriseName?: string }>
27
+ 'organization-created': CustomEvent<{ organizationId: string; type: 'enterprise' | 'team' }>
28
+ 'enterprise-authentication-requested': CustomEvent<{ organizationId: string }>
29
+ 'enterprise-authentication-deferred': CustomEvent<{ organizationId: string }>
30
+ 'team-upgrade-requested': CustomEvent<{ organizationId: string }>
31
+ 'team-invite-members-requested': CustomEvent<{ organizationId: string }>
32
+ error: CustomEvent<{
33
+ code: 'ENTERPRISE_CREATE_FAILED' | 'TEAM_CREATE_FAILED'
34
+ message: string
35
+ recoverable: true
36
+ }>
37
+ }
38
+
39
+ type OnboardingChoice = 'enterprise' | 'team'
40
+ type SkippableStep = 'method-selection' | 'enterprise-form' | 'team-form'
41
+
42
+ interface EnterpriseErrors {
43
+ organizationName: string
44
+ enterpriseName: string
45
+ unifiedSocialCreditCode: string
46
+ global: string
47
+ canRequestJoin: boolean
48
+ }
49
+
50
+ interface TeamErrors {
51
+ teamName: string
52
+ global: string
53
+ }
54
+
55
+ const EMPTY_ENTERPRISE_ERRORS: EnterpriseErrors = {
56
+ organizationName: '',
57
+ enterpriseName: '',
58
+ unifiedSocialCreditCode: '',
59
+ global: '',
60
+ canRequestJoin: false,
61
+ }
62
+
63
+ const EMPTY_TEAM_ERRORS: TeamErrors = {
64
+ teamName: '',
65
+ global: '',
66
+ }
67
+
68
+ /**
69
+ * Internal implementation used exclusively by <uniplat-account-context>.
70
+ * It is deliberately not exported from the package entry point.
71
+ */
72
+ export class InternalOrganizationOnboarding extends LitElement {
73
+ static override properties = {
74
+ step: { state: true },
75
+ choice: { state: true },
76
+ organizationName: { state: true },
77
+ enterpriseName: { state: true },
78
+ unifiedSocialCreditCode: { state: true },
79
+ teamName: { state: true },
80
+ enterpriseErrors: { state: true },
81
+ teamErrors: { state: true },
82
+ submitting: { state: true },
83
+ createdOrganization: { state: true },
84
+ }
85
+
86
+ static override styles = css`
87
+ :host {
88
+ --uniplat-onboarding-width: 400px;
89
+ --uniplat-onboarding-primary: #2e6bf5;
90
+ --uniplat-onboarding-text: #182634;
91
+ --uniplat-onboarding-muted: #7a8794;
92
+ --uniplat-onboarding-border: #e4ebf3;
93
+ --uniplat-onboarding-error: #e5484d;
94
+ --uniplat-onboarding-font-family:
95
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
96
+ display: block;
97
+ width: min(var(--uniplat-onboarding-width), 100%);
98
+ color: var(--uniplat-onboarding-text);
99
+ font-family: var(--uniplat-onboarding-font-family);
100
+ line-height: 1.5;
101
+ }
102
+
103
+ * {
104
+ box-sizing: border-box;
105
+ }
106
+
107
+ button,
108
+ input {
109
+ font: inherit;
110
+ }
111
+
112
+ button {
113
+ cursor: pointer;
114
+ }
115
+
116
+ .card {
117
+ width: 100%;
118
+ overflow: hidden;
119
+ border: 1px solid var(--uniplat-onboarding-border);
120
+ border-radius: 16px;
121
+ background: #ffffff;
122
+ box-shadow: 0 8px 24px rgba(20, 40, 80, 0.08);
123
+ }
124
+
125
+ .content {
126
+ padding: 24px;
127
+ }
128
+
129
+ .heading-row {
130
+ display: flex;
131
+ align-items: flex-start;
132
+ justify-content: space-between;
133
+ gap: 16px;
134
+ }
135
+
136
+ h2,
137
+ p {
138
+ margin: 0;
139
+ }
140
+
141
+ h2 {
142
+ color: #182634;
143
+ font-size: 20px;
144
+ font-weight: 800;
145
+ line-height: 1.35;
146
+ }
147
+
148
+ .description {
149
+ margin-top: 8px;
150
+ color: var(--uniplat-onboarding-muted);
151
+ font-size: 13px;
152
+ line-height: 1.7;
153
+ }
154
+
155
+ .skip {
156
+ flex: none;
157
+ padding: 2px 0;
158
+ border: 0;
159
+ color: #98a4b2;
160
+ background: transparent;
161
+ font-size: 12.5px;
162
+ }
163
+
164
+ .options {
165
+ display: grid;
166
+ gap: 10px;
167
+ margin-top: 20px;
168
+ }
169
+
170
+ .option {
171
+ display: grid;
172
+ grid-template-columns: 38px minmax(0, 1fr) 18px;
173
+ align-items: center;
174
+ gap: 11px;
175
+ width: 100%;
176
+ min-height: 66px;
177
+ padding: 10px 12px;
178
+ border: 1px solid #e4ebf3;
179
+ border-radius: 10px;
180
+ color: inherit;
181
+ background: #ffffff;
182
+ text-align: left;
183
+ }
184
+
185
+ .option.selected {
186
+ border-color: var(--uniplat-onboarding-primary);
187
+ background: #f7f9ff;
188
+ box-shadow: 0 0 0 1px rgba(46, 107, 245, 0.08);
189
+ }
190
+
191
+ .option-icon,
192
+ .success-icon,
193
+ .empty-icon {
194
+ display: grid;
195
+ place-items: center;
196
+ border-radius: 10px;
197
+ }
198
+
199
+ .option-icon {
200
+ width: 38px;
201
+ height: 38px;
202
+ color: var(--uniplat-onboarding-primary);
203
+ background: #e6eefe;
204
+ }
205
+
206
+ .option.team .option-icon {
207
+ color: #0ca678;
208
+ background: #e3f7ef;
209
+ }
210
+
211
+ .option-title {
212
+ display: block;
213
+ font-size: 14px;
214
+ font-weight: 750;
215
+ }
216
+
217
+ .option-meta {
218
+ display: block;
219
+ margin-top: 2px;
220
+ color: #8b98a6;
221
+ font-size: 11.5px;
222
+ line-height: 1.45;
223
+ }
224
+
225
+ .selected-mark {
226
+ display: grid;
227
+ place-items: center;
228
+ width: 18px;
229
+ height: 18px;
230
+ border: 1.5px solid #c7d1df;
231
+ border-radius: 50%;
232
+ color: transparent;
233
+ }
234
+
235
+ .option.selected .selected-mark {
236
+ border-color: var(--uniplat-onboarding-primary);
237
+ color: #ffffff;
238
+ background: var(--uniplat-onboarding-primary);
239
+ }
240
+
241
+ .primary,
242
+ .secondary {
243
+ display: inline-flex;
244
+ align-items: center;
245
+ justify-content: center;
246
+ min-height: 46px;
247
+ border-radius: 10px;
248
+ font-size: 14px;
249
+ font-weight: 700;
250
+ }
251
+
252
+ .primary {
253
+ width: 100%;
254
+ margin-top: 16px;
255
+ border: 0;
256
+ color: #ffffff;
257
+ background: var(--uniplat-onboarding-primary);
258
+ }
259
+
260
+ .primary:disabled {
261
+ cursor: not-allowed;
262
+ background: #a9bff6;
263
+ }
264
+
265
+ .secondary {
266
+ border: 1px solid #dce5f0;
267
+ color: #5a6673;
268
+ background: #ffffff;
269
+ }
270
+
271
+ .form {
272
+ display: grid;
273
+ gap: 15px;
274
+ margin-top: 20px;
275
+ }
276
+
277
+ .field label {
278
+ display: block;
279
+ margin-bottom: 7px;
280
+ color: #25303f;
281
+ font-size: 13px;
282
+ font-weight: 700;
283
+ }
284
+
285
+ .required {
286
+ color: var(--uniplat-onboarding-error);
287
+ }
288
+
289
+ .field input {
290
+ width: 100%;
291
+ height: 44px;
292
+ padding: 0 12px;
293
+ border: 1px solid #e2e9f3;
294
+ border-radius: 9px;
295
+ outline: none;
296
+ color: #25303f;
297
+ background: #ffffff;
298
+ font-size: 13.5px;
299
+ }
300
+
301
+ .field input:focus {
302
+ border-color: var(--uniplat-onboarding-primary);
303
+ box-shadow: 0 0 0 3px rgba(46, 107, 245, 0.1);
304
+ }
305
+
306
+ .field input[aria-invalid='true'] {
307
+ border-color: var(--uniplat-onboarding-error);
308
+ box-shadow: none;
309
+ }
310
+
311
+ .helper,
312
+ .field-error {
313
+ display: block;
314
+ margin-top: 6px;
315
+ font-size: 11.5px;
316
+ line-height: 1.55;
317
+ }
318
+
319
+ .helper {
320
+ color: #98a4b2;
321
+ }
322
+
323
+ .field-error {
324
+ color: var(--uniplat-onboarding-error);
325
+ }
326
+
327
+ .inline-link {
328
+ padding: 0;
329
+ border: 0;
330
+ color: var(--uniplat-onboarding-primary);
331
+ background: transparent;
332
+ font-weight: 700;
333
+ }
334
+
335
+ .summary-error,
336
+ .team-notice {
337
+ display: flex;
338
+ align-items: flex-start;
339
+ gap: 8px;
340
+ padding: 10px 12px;
341
+ border-radius: 9px;
342
+ font-size: 12px;
343
+ line-height: 1.6;
344
+ }
345
+
346
+ .summary-error {
347
+ margin-top: 16px;
348
+ color: #c7373c;
349
+ background: #fdeced;
350
+ }
351
+
352
+ .team-notice {
353
+ margin-top: 16px;
354
+ color: #147552;
355
+ background: #eaf9f3;
356
+ }
357
+
358
+ .confirmation {
359
+ padding: 24px;
360
+ }
361
+
362
+ .confirmation-icon {
363
+ display: grid;
364
+ place-items: center;
365
+ width: 42px;
366
+ height: 42px;
367
+ margin-bottom: 16px;
368
+ border-radius: 12px;
369
+ color: #e8590c;
370
+ background: #fff4e2;
371
+ }
372
+
373
+ .confirmation-actions,
374
+ .success-actions {
375
+ display: grid;
376
+ grid-template-columns: 1fr 1fr;
377
+ gap: 10px;
378
+ margin-top: 20px;
379
+ }
380
+
381
+ .confirmation-actions .primary,
382
+ .success-actions .primary {
383
+ margin-top: 0;
384
+ }
385
+
386
+ .success {
387
+ padding: 30px 24px 24px;
388
+ text-align: center;
389
+ }
390
+
391
+ .success-icon,
392
+ .empty-icon {
393
+ width: 56px;
394
+ height: 56px;
395
+ margin: 0 auto 16px;
396
+ color: #0ca678;
397
+ background: #e3f7ef;
398
+ }
399
+
400
+ .empty-icon {
401
+ color: var(--uniplat-onboarding-primary);
402
+ background: #e6eefe;
403
+ }
404
+
405
+ .success .description {
406
+ max-width: 320px;
407
+ margin-right: auto;
408
+ margin-left: auto;
409
+ }
410
+
411
+ .status-badge {
412
+ display: inline-flex;
413
+ align-items: center;
414
+ margin-top: 12px;
415
+ padding: 3px 9px;
416
+ border-radius: 999px;
417
+ color: #e8590c;
418
+ background: #fff4e2;
419
+ font-size: 11.5px;
420
+ font-weight: 700;
421
+ }
422
+
423
+ .status-badge.team {
424
+ color: #0ca678;
425
+ background: #e3f7ef;
426
+ }
427
+
428
+ .empty-actions {
429
+ display: grid;
430
+ gap: 10px;
431
+ margin-top: 20px;
432
+ }
433
+
434
+ .empty-actions .primary {
435
+ margin-top: 0;
436
+ }
437
+
438
+ .text-action {
439
+ padding: 4px;
440
+ border: 0;
441
+ color: #8b98a6;
442
+ background: transparent;
443
+ font-size: 12.5px;
444
+ }
445
+
446
+ svg {
447
+ display: block;
448
+ width: 22px;
449
+ height: 22px;
450
+ fill: none;
451
+ stroke: currentColor;
452
+ stroke-linecap: round;
453
+ stroke-linejoin: round;
454
+ stroke-width: 1.8;
455
+ }
456
+ `
457
+
458
+ declare private step: OrganizationOnboardingStep
459
+ declare private choice: OnboardingChoice
460
+ declare private organizationName: string
461
+ declare private enterpriseName: string
462
+ declare private unifiedSocialCreditCode: string
463
+ declare private teamName: string
464
+ declare private enterpriseErrors: EnterpriseErrors
465
+ declare private teamErrors: TeamErrors
466
+ declare private submitting: boolean
467
+ declare private createdOrganization: CreatedOnboardingOrganization | null
468
+
469
+ private transport: OrganizationOnboardingTransport | null = null
470
+ private skipReturnStep: SkippableStep = 'method-selection'
471
+
472
+ constructor() {
473
+ super()
474
+ this.step = 'method-selection'
475
+ this.choice = 'enterprise'
476
+ this.organizationName = ''
477
+ this.enterpriseName = ''
478
+ this.unifiedSocialCreditCode = ''
479
+ this.teamName = ''
480
+ this.enterpriseErrors = { ...EMPTY_ENTERPRISE_ERRORS }
481
+ this.teamErrors = { ...EMPTY_TEAM_ERRORS }
482
+ this.submitting = false
483
+ this.createdOrganization = null
484
+ }
485
+
486
+ configure(config: OrganizationOnboardingConfig): void {
487
+ if (
488
+ !config?.transport ||
489
+ typeof config.transport.createEnterprise !== 'function' ||
490
+ typeof config.transport.createTeam !== 'function'
491
+ ) {
492
+ throw new TypeError('organization-onboarding requires a complete transport')
493
+ }
494
+ if (config.initialStep && !['unassigned', 'method-selection'].includes(config.initialStep)) {
495
+ throw new TypeError('organization-onboarding initialStep is invalid')
496
+ }
497
+ this.transport = config.transport
498
+ this.reset(config.initialStep ?? 'method-selection')
499
+ }
500
+
501
+ start(step: 'unassigned' | 'method-selection' = 'method-selection'): void {
502
+ if (!this.transport) throw new Error('organization-onboarding is not configured')
503
+ this.reset(step)
504
+ }
505
+
506
+ override render() {
507
+ if (!this.transport) return nothing
508
+ if (this.step === 'unassigned') return this.renderUnassigned()
509
+ if (this.step === 'method-selection') return this.renderMethodSelection()
510
+ if (this.step === 'skip-confirmation') return this.renderSkipConfirmation()
511
+ if (this.step === 'enterprise-form') return this.renderEnterpriseForm()
512
+ if (this.step === 'team-form') return this.renderTeamForm()
513
+ return this.renderSuccess()
514
+ }
515
+
516
+ private renderUnassigned() {
517
+ return html`
518
+ <section class="card" part="shell" aria-label="无归属团队或企业">
519
+ <div class="success" part="unassigned">
520
+ <div class="empty-icon" part="unassigned-icon">${icon('building-plus')}</div>
521
+ <h2 part="title">你还没有归属的团队 / 企业</h2>
522
+ <p class="description" part="description">
523
+ 创建一个团队 / 企业,即可办理社保、薪酬、用工等业务;也可以用邀请码加入已有的团队 /
524
+ 企业。
525
+ </p>
526
+ <div class="empty-actions">
527
+ <button
528
+ class="primary"
529
+ part="primary-action"
530
+ type="button"
531
+ @click=${this.openMethodSelection}
532
+ >
533
+ 创建团队 / 企业
534
+ </button>
535
+ <button
536
+ class="secondary"
537
+ part="secondary-action"
538
+ type="button"
539
+ @click=${() => this.requestJoin()}
540
+ >
541
+ 用邀请码加入
542
+ </button>
543
+ <button
544
+ class="text-action"
545
+ part="skip-action"
546
+ type="button"
547
+ @click=${this.skipUnassigned}
548
+ >
549
+ 暂不创建,先逛逛
550
+ </button>
551
+ </div>
552
+ </div>
553
+ </section>
554
+ `
555
+ }
556
+
557
+ private renderMethodSelection() {
558
+ return html`
559
+ <section class="card" part="shell" aria-label="选择入驻方式">
560
+ <div class="content">
561
+ <div class="heading-row">
562
+ <div>
563
+ <h2 part="title">创建你的团队 / 企业</h2>
564
+ <p class="description" part="description">
565
+ 办理社保、薪酬、用工等业务需先归属一个团队 / 企业
566
+ </p>
567
+ </div>
568
+ <button
569
+ class="skip"
570
+ part="skip-action"
571
+ type="button"
572
+ @click=${() => this.openSkip('method-selection')}
573
+ >
574
+ 跳过
575
+ </button>
576
+ </div>
577
+ <div class="options" role="list" part="method-options">
578
+ ${this.renderOption(
579
+ 'enterprise',
580
+ 'building',
581
+ '创建企业',
582
+ '有营业执照,需填写企业信息、需认证',
583
+ )}
584
+ ${this.renderOption(
585
+ 'team',
586
+ 'users',
587
+ '创建团队',
588
+ '无需企业信息、无需认证,可随时升级为企业',
589
+ )}
590
+ </div>
591
+ <button
592
+ class="primary"
593
+ part="primary-action"
594
+ type="button"
595
+ @click=${this.continueFromChoice}
596
+ >
597
+ 下一步
598
+ </button>
599
+ </div>
600
+ </section>
601
+ `
602
+ }
603
+
604
+ private renderOption(
605
+ choice: OnboardingChoice,
606
+ iconName: IconName,
607
+ title: string,
608
+ description: string,
609
+ ) {
610
+ const selected = this.choice === choice
611
+ return html`
612
+ <button
613
+ class="option ${choice === 'team' ? 'team' : ''} ${selected ? 'selected' : ''}"
614
+ part="method-option"
615
+ type="button"
616
+ aria-pressed=${String(selected)}
617
+ @click=${() => {
618
+ this.choice = choice
619
+ }}
620
+ >
621
+ <span class="option-icon">${icon(iconName)}</span>
622
+ <span>
623
+ <span class="option-title">${title}</span>
624
+ <span class="option-meta">${description}</span>
625
+ </span>
626
+ <span class="selected-mark">${icon('check')}</span>
627
+ </button>
628
+ `
629
+ }
630
+
631
+ private renderSkipConfirmation() {
632
+ return html`
633
+ <section class="card" part="shell" aria-label="确认跳过入驻">
634
+ <div class="confirmation" part="skip-confirmation">
635
+ <div class="confirmation-icon">${icon('alert')}</div>
636
+ <h2 part="title">确定暂不创建团队 / 企业?</h2>
637
+ <p class="description" part="description">
638
+ 跳过后你仍可浏览官网与服务介绍,但无法办理社保、薪酬、用工等业务。随时可在顶部导航“创建团队
639
+ / 企业”补充。
640
+ </p>
641
+ <div class="confirmation-actions">
642
+ <button
643
+ class="secondary"
644
+ part="secondary-action"
645
+ type="button"
646
+ @click=${this.confirmSkip}
647
+ >
648
+ 跳过,先逛逛
649
+ </button>
650
+ <button
651
+ class="primary"
652
+ part="primary-action"
653
+ type="button"
654
+ @click=${this.continueCreating}
655
+ >
656
+ 继续创建
657
+ </button>
658
+ </div>
659
+ </div>
660
+ </section>
661
+ `
662
+ }
663
+
664
+ private renderEnterpriseForm() {
665
+ const errorCount = [
666
+ this.enterpriseErrors.organizationName,
667
+ this.enterpriseErrors.enterpriseName,
668
+ this.enterpriseErrors.unifiedSocialCreditCode,
669
+ ].filter(Boolean).length
670
+ return html`
671
+ <section class="card" part="shell" aria-label="填写企业信息">
672
+ <div class="content">
673
+ <div class="heading-row">
674
+ <div><h2 part="title">填写企业信息</h2></div>
675
+ <button
676
+ class="skip"
677
+ part="skip-action"
678
+ type="button"
679
+ @click=${() => this.openSkip('enterprise-form')}
680
+ >
681
+ 跳过
682
+ </button>
683
+ </div>
684
+ ${
685
+ errorCount > 0
686
+ ? html`<div class="summary-error" part="error-summary" role="alert">
687
+ ${icon('alert')}<span>有 ${errorCount} 项信息需要修改,请检查后重新提交</span>
688
+ </div>`
689
+ : nothing
690
+ }
691
+ <form class="form" part="form" @submit=${this.submitEnterprise} novalidate>
692
+ ${this.renderEnterpriseField(
693
+ 'organization-name',
694
+ '组织名称',
695
+ this.organizationName,
696
+ '平台内显示的简称,成员在切换时看到的名字',
697
+ this.enterpriseErrors.organizationName,
698
+ 30,
699
+ (value) => {
700
+ this.organizationName = value
701
+ this.enterpriseErrors = {
702
+ ...this.enterpriseErrors,
703
+ organizationName: '',
704
+ global: '',
705
+ }
706
+ },
707
+ )}
708
+ ${this.renderEnterpriseField(
709
+ 'enterprise-name',
710
+ '企业名称',
711
+ this.enterpriseName,
712
+ '需与营业执照一致,将用于合同与开票',
713
+ this.enterpriseErrors.enterpriseName,
714
+ 100,
715
+ (value) => {
716
+ this.enterpriseName = value
717
+ this.enterpriseErrors = {
718
+ ...this.enterpriseErrors,
719
+ enterpriseName: '',
720
+ global: '',
721
+ canRequestJoin: false,
722
+ }
723
+ },
724
+ this.enterpriseErrors.canRequestJoin,
725
+ )}
726
+ ${this.renderEnterpriseField(
727
+ 'credit-code',
728
+ '统一社会信用代码',
729
+ this.unifiedSocialCreditCode,
730
+ '',
731
+ this.enterpriseErrors.unifiedSocialCreditCode,
732
+ 18,
733
+ (value) => {
734
+ this.unifiedSocialCreditCode = value.toUpperCase().replace(/\s/g, '')
735
+ this.enterpriseErrors = {
736
+ ...this.enterpriseErrors,
737
+ unifiedSocialCreditCode: '',
738
+ global: '',
739
+ }
740
+ },
741
+ )}
742
+ ${
743
+ this.enterpriseErrors.global
744
+ ? html`<div class="field-error" part="global-error" role="alert">
745
+ ${this.enterpriseErrors.global}
746
+ </div>`
747
+ : nothing
748
+ }
749
+ <button
750
+ class="primary"
751
+ part="primary-action"
752
+ type="submit"
753
+ ?disabled=${this.submitting}
754
+ >
755
+ ${this.submitting ? '正在创建…' : '创建企业'}
756
+ </button>
757
+ </form>
758
+ </div>
759
+ </section>
760
+ `
761
+ }
762
+
763
+ private renderEnterpriseField(
764
+ id: string,
765
+ label: string,
766
+ value: string,
767
+ helper: string,
768
+ error: string,
769
+ maxLength: number,
770
+ update: (value: string) => void,
771
+ canRequestJoin = false,
772
+ ) {
773
+ return html`
774
+ <div class="field" part="field">
775
+ <label for=${id}>${label} <span class="required">*</span></label>
776
+ <input
777
+ id=${id}
778
+ part="input"
779
+ .value=${value}
780
+ maxlength=${maxLength}
781
+ aria-invalid=${String(Boolean(error))}
782
+ @input=${(event: Event) => update((event.currentTarget as HTMLInputElement).value)}
783
+ @blur=${() => this.validateEnterpriseField(id)}
784
+ />
785
+ ${helper && !error ? html`<span class="helper">${helper}</span>` : nothing}
786
+ ${
787
+ error
788
+ ? html`<span class="field-error" part="field-error" role="alert">
789
+ ${error}
790
+ ${
791
+ canRequestJoin
792
+ ? html`,可
793
+ <button
794
+ class="inline-link"
795
+ type="button"
796
+ @click=${() => this.requestJoin(this.enterpriseName)}
797
+ >
798
+ 申请加入
799
+ </button>
800
+ 或联系管理员`
801
+ : nothing
802
+ }
803
+ </span>`
804
+ : nothing
805
+ }
806
+ </div>
807
+ `
808
+ }
809
+
810
+ private renderTeamForm() {
811
+ return html`
812
+ <section class="card" part="shell" aria-label="填写团队信息">
813
+ <div class="content">
814
+ <div class="heading-row">
815
+ <div><h2 part="title">填写团队信息</h2></div>
816
+ <button
817
+ class="skip"
818
+ part="skip-action"
819
+ type="button"
820
+ @click=${() => this.openSkip('team-form')}
821
+ >
822
+ 跳过
823
+ </button>
824
+ </div>
825
+ <div class="team-notice" part="team-notice">
826
+ ${icon('check')}<span
827
+ >团队无需营业执照与企业信息,创建后即可协作;后续可一键升级为企业。</span
828
+ >
829
+ </div>
830
+ <form class="form" part="form" @submit=${this.submitTeam} novalidate>
831
+ <div class="field" part="field">
832
+ <label for="team-name">团队名称 <span class="required">*</span></label>
833
+ <input
834
+ id="team-name"
835
+ part="input"
836
+ .value=${this.teamName}
837
+ maxlength="30"
838
+ aria-invalid=${String(Boolean(this.teamErrors.teamName))}
839
+ @input=${(event: Event) => {
840
+ this.teamName = (event.currentTarget as HTMLInputElement).value
841
+ this.teamErrors = { ...this.teamErrors, teamName: '', global: '' }
842
+ }}
843
+ @blur=${this.validateTeamName}
844
+ />
845
+ ${
846
+ this.teamErrors.teamName
847
+ ? html`<span class="field-error" part="field-error" role="alert">
848
+ ${this.teamErrors.teamName}
849
+ </span>`
850
+ : html`<span class="helper">2-30 个字,团队成员可见</span>`
851
+ }
852
+ </div>
853
+ ${
854
+ this.teamErrors.global
855
+ ? html`<div class="field-error" part="global-error" role="alert">
856
+ ${this.teamErrors.global}
857
+ </div>`
858
+ : nothing
859
+ }
860
+ <button
861
+ class="primary"
862
+ part="primary-action"
863
+ type="submit"
864
+ ?disabled=${this.submitting}
865
+ >
866
+ ${this.submitting ? '正在创建…' : '创建团队'}
867
+ </button>
868
+ </form>
869
+ </div>
870
+ </section>
871
+ `
872
+ }
873
+
874
+ private renderSuccess() {
875
+ const organization = this.createdOrganization
876
+ if (!organization) return nothing
877
+ const enterprise = organization.type === 'enterprise'
878
+ return html`
879
+ <section class="card" part="shell" aria-label=${enterprise ? '企业创建成功' : '团队创建成功'}>
880
+ <div class="success" part="success">
881
+ <div class="success-icon" part="success-icon">
882
+ ${icon(enterprise ? 'building-check' : 'users')}
883
+ </div>
884
+ <h2 part="title">${organization.name} 已创建</h2>
885
+ <span class="status-badge ${enterprise ? '' : 'team'}" part="status-badge">
886
+ ${enterprise ? '未认证' : '无需认证'}
887
+ </span>
888
+ <p class="description" part="description">
889
+ ${
890
+ enterprise
891
+ ? '未认证可先浏览与咨询;完成企业认证后才能签约下单、开票与办理社保业务。'
892
+ : '团队可直接使用协作与部分服务;若需签约下单、开票,请升级为企业并完成认证。'
893
+ }
894
+ </p>
895
+ <div class="success-actions">
896
+ ${
897
+ enterprise
898
+ ? html`
899
+ <button
900
+ class="secondary"
901
+ part="secondary-action"
902
+ type="button"
903
+ @click=${this.deferAuthentication}
904
+ >
905
+ 稍后认证
906
+ </button>
907
+ <button
908
+ class="primary"
909
+ part="primary-action"
910
+ type="button"
911
+ @click=${this.requestAuthentication}
912
+ >
913
+ 去认证
914
+ </button>
915
+ `
916
+ : html`
917
+ <button
918
+ class="secondary"
919
+ part="secondary-action"
920
+ type="button"
921
+ @click=${this.requestTeamUpgrade}
922
+ >
923
+ 升级为企业
924
+ </button>
925
+ <button
926
+ class="primary"
927
+ part="primary-action"
928
+ type="button"
929
+ @click=${this.requestTeamInvites}
930
+ >
931
+ 邀请成员
932
+ </button>
933
+ `
934
+ }
935
+ </div>
936
+ </div>
937
+ </section>
938
+ `
939
+ }
940
+
941
+ private reset(step: 'unassigned' | 'method-selection') {
942
+ this.step = step
943
+ this.choice = 'enterprise'
944
+ this.organizationName = ''
945
+ this.enterpriseName = ''
946
+ this.unifiedSocialCreditCode = ''
947
+ this.teamName = ''
948
+ this.enterpriseErrors = { ...EMPTY_ENTERPRISE_ERRORS }
949
+ this.teamErrors = { ...EMPTY_TEAM_ERRORS }
950
+ this.submitting = false
951
+ this.createdOrganization = null
952
+ }
953
+
954
+ private readonly openMethodSelection = () => {
955
+ this.step = 'method-selection'
956
+ }
957
+
958
+ private readonly continueFromChoice = () => {
959
+ this.step = this.choice === 'enterprise' ? 'enterprise-form' : 'team-form'
960
+ }
961
+
962
+ private openSkip(returnStep: SkippableStep) {
963
+ this.skipReturnStep = returnStep
964
+ this.step = 'skip-confirmation'
965
+ }
966
+
967
+ private readonly continueCreating = () => {
968
+ this.step = this.skipReturnStep
969
+ }
970
+
971
+ private readonly confirmSkip = () => {
972
+ this.dispatchPublicEvent('onboarding-skipped', { source: this.skipReturnStep })
973
+ }
974
+
975
+ private readonly skipUnassigned = () => {
976
+ this.dispatchPublicEvent('onboarding-skipped', { source: 'unassigned' })
977
+ }
978
+
979
+ private requestJoin(enterpriseName?: string) {
980
+ this.dispatchPublicEvent(
981
+ 'join-organization-requested',
982
+ enterpriseName ? { enterpriseName } : {},
983
+ )
984
+ }
985
+
986
+ private validateEnterpriseField(id: string) {
987
+ if (id === 'organization-name' && !this.organizationName.trim()) {
988
+ this.enterpriseErrors = { ...this.enterpriseErrors, organizationName: '此项为必填' }
989
+ }
990
+ if (id === 'enterprise-name' && !this.enterpriseName.trim()) {
991
+ this.enterpriseErrors = { ...this.enterpriseErrors, enterpriseName: '此项为必填' }
992
+ }
993
+ if (id === 'credit-code') {
994
+ const creditCode = this.unifiedSocialCreditCode.trim().toUpperCase()
995
+ this.enterpriseErrors = {
996
+ ...this.enterpriseErrors,
997
+ unifiedSocialCreditCode: /^[0-9A-Z]{18}$/.test(creditCode)
998
+ ? ''
999
+ : '格式有误,应为 18 位数字与大写字母',
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ private validateEnterprise(): boolean {
1005
+ const organizationName = this.organizationName.trim()
1006
+ const enterpriseName = this.enterpriseName.trim()
1007
+ const creditCode = this.unifiedSocialCreditCode.trim().toUpperCase()
1008
+ this.enterpriseErrors = {
1009
+ ...EMPTY_ENTERPRISE_ERRORS,
1010
+ organizationName: organizationName ? '' : '此项为必填',
1011
+ enterpriseName: enterpriseName ? '' : '此项为必填',
1012
+ unifiedSocialCreditCode: /^[0-9A-Z]{18}$/.test(creditCode)
1013
+ ? ''
1014
+ : '格式有误,应为 18 位数字与大写字母',
1015
+ }
1016
+ return Boolean(organizationName && enterpriseName && /^[0-9A-Z]{18}$/.test(creditCode))
1017
+ }
1018
+
1019
+ private readonly submitEnterprise = async (event: SubmitEvent) => {
1020
+ event.preventDefault()
1021
+ const transport = this.transport
1022
+ if (!transport || this.submitting || !this.validateEnterprise()) return
1023
+ this.submitting = true
1024
+ try {
1025
+ const organization = normalizeCreatedOrganization(
1026
+ await transport.createEnterprise({
1027
+ organizationName: this.organizationName.trim(),
1028
+ enterpriseName: this.enterpriseName.trim(),
1029
+ unifiedSocialCreditCode: this.unifiedSocialCreditCode.trim().toUpperCase(),
1030
+ }),
1031
+ 'enterprise',
1032
+ )
1033
+ this.createdOrganization = organization
1034
+ this.step = 'enterprise-success'
1035
+ this.dispatchPublicEvent('organization-created', {
1036
+ organizationId: organization.id,
1037
+ type: 'enterprise',
1038
+ })
1039
+ } catch (error) {
1040
+ this.handleEnterpriseError(error)
1041
+ } finally {
1042
+ this.submitting = false
1043
+ }
1044
+ }
1045
+
1046
+ private handleEnterpriseError(error: unknown) {
1047
+ const code = error instanceof OrganizationOnboardingTransportError ? error.code : 'UNKNOWN'
1048
+ if (code === 'ENTERPRISE_NAME_CONFLICT') {
1049
+ this.enterpriseErrors = {
1050
+ ...this.enterpriseErrors,
1051
+ enterpriseName: '该企业已被创建',
1052
+ canRequestJoin: true,
1053
+ }
1054
+ return
1055
+ }
1056
+ if (code === 'CREDIT_CODE_CONFLICT') {
1057
+ this.enterpriseErrors = {
1058
+ ...this.enterpriseErrors,
1059
+ unifiedSocialCreditCode: '该统一社会信用代码已关联企业',
1060
+ }
1061
+ return
1062
+ }
1063
+ this.enterpriseErrors = {
1064
+ ...this.enterpriseErrors,
1065
+ global: '企业创建失败,请稍后重试',
1066
+ }
1067
+ this.dispatchPublicEvent('error', {
1068
+ code: 'ENTERPRISE_CREATE_FAILED',
1069
+ message: '企业创建失败,请稍后重试',
1070
+ recoverable: true,
1071
+ })
1072
+ }
1073
+
1074
+ private readonly validateTeamName = () => {
1075
+ const length = Array.from(this.teamName.trim()).length
1076
+ this.teamErrors = {
1077
+ ...this.teamErrors,
1078
+ teamName: length >= 2 && length <= 30 ? '' : '请输入 2-30 个字的团队名称',
1079
+ }
1080
+ }
1081
+
1082
+ private readonly submitTeam = async (event: SubmitEvent) => {
1083
+ event.preventDefault()
1084
+ const transport = this.transport
1085
+ const teamName = this.teamName.trim()
1086
+ const length = Array.from(teamName).length
1087
+ if (!transport || this.submitting) return
1088
+ if (length < 2 || length > 30) {
1089
+ this.validateTeamName()
1090
+ return
1091
+ }
1092
+ this.submitting = true
1093
+ this.teamErrors = { ...EMPTY_TEAM_ERRORS }
1094
+ try {
1095
+ const organization = normalizeCreatedOrganization(
1096
+ await transport.createTeam({ teamName }),
1097
+ 'team',
1098
+ )
1099
+ this.createdOrganization = organization
1100
+ this.step = 'team-success'
1101
+ this.dispatchPublicEvent('organization-created', {
1102
+ organizationId: organization.id,
1103
+ type: 'team',
1104
+ })
1105
+ } catch (error) {
1106
+ if (
1107
+ error instanceof OrganizationOnboardingTransportError &&
1108
+ error.code === 'TEAM_NAME_CONFLICT'
1109
+ ) {
1110
+ this.teamErrors = { ...this.teamErrors, teamName: '你已有同名团队,请更换名称' }
1111
+ } else {
1112
+ this.teamErrors = { ...this.teamErrors, global: '团队创建失败,请稍后重试' }
1113
+ this.dispatchPublicEvent('error', {
1114
+ code: 'TEAM_CREATE_FAILED',
1115
+ message: '团队创建失败,请稍后重试',
1116
+ recoverable: true,
1117
+ })
1118
+ }
1119
+ } finally {
1120
+ this.submitting = false
1121
+ }
1122
+ }
1123
+
1124
+ private readonly requestAuthentication = () => {
1125
+ const organizationId = this.createdOrganization?.id
1126
+ if (organizationId) {
1127
+ this.dispatchPublicEvent('enterprise-authentication-requested', { organizationId })
1128
+ }
1129
+ }
1130
+
1131
+ private readonly deferAuthentication = () => {
1132
+ const organizationId = this.createdOrganization?.id
1133
+ if (organizationId) {
1134
+ this.dispatchPublicEvent('enterprise-authentication-deferred', { organizationId })
1135
+ }
1136
+ }
1137
+
1138
+ private readonly requestTeamUpgrade = () => {
1139
+ const organizationId = this.createdOrganization?.id
1140
+ if (organizationId) {
1141
+ this.dispatchPublicEvent('team-upgrade-requested', { organizationId })
1142
+ }
1143
+ }
1144
+
1145
+ private readonly requestTeamInvites = () => {
1146
+ const organizationId = this.createdOrganization?.id
1147
+ if (organizationId) {
1148
+ this.dispatchPublicEvent('team-invite-members-requested', { organizationId })
1149
+ }
1150
+ }
1151
+
1152
+ private dispatchPublicEvent<K extends keyof OrganizationOnboardingEventMap>(
1153
+ type: K,
1154
+ detail: OrganizationOnboardingEventMap[K]['detail'],
1155
+ ) {
1156
+ this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }))
1157
+ }
1158
+ }
1159
+
1160
+ export const INTERNAL_ORGANIZATION_ONBOARDING_TAG =
1161
+ 'uniplat-internal-organization-onboarding' as const
1162
+
1163
+ if (
1164
+ typeof customElements !== 'undefined' &&
1165
+ !customElements.get(INTERNAL_ORGANIZATION_ONBOARDING_TAG)
1166
+ ) {
1167
+ customElements.define(INTERNAL_ORGANIZATION_ONBOARDING_TAG, InternalOrganizationOnboarding)
1168
+ }
1169
+
1170
+ declare global {
1171
+ interface HTMLElementTagNameMap {
1172
+ 'uniplat-internal-organization-onboarding': InternalOrganizationOnboarding
1173
+ }
1174
+ }
1175
+
1176
+ function normalizeCreatedOrganization(
1177
+ value: CreatedOnboardingOrganization,
1178
+ expectedType: 'enterprise' | 'team',
1179
+ ): CreatedOnboardingOrganization {
1180
+ if (!value || value.type !== expectedType) {
1181
+ throw new OrganizationOnboardingTransportError('UNKNOWN')
1182
+ }
1183
+ const id = normalizeText(value.id, 128)
1184
+ const name = normalizeText(value.name, 100)
1185
+ if (!id || !name) throw new OrganizationOnboardingTransportError('UNKNOWN')
1186
+ return { id, name, type: expectedType }
1187
+ }
1188
+
1189
+ function normalizeText(value: unknown, maxLength: number): string {
1190
+ return typeof value === 'string' ? value.trim().slice(0, maxLength) : ''
1191
+ }
1192
+
1193
+ type IconName = 'alert' | 'building' | 'building-check' | 'building-plus' | 'check' | 'users'
1194
+
1195
+ function icon(name: IconName): TemplateResult {
1196
+ if (name === 'building' || name === 'building-check' || name === 'building-plus') {
1197
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true">
1198
+ <path d="M5 21V6l7-3 7 3v15M3 21h18" />
1199
+ <path d="M9 8h1M14 8h1M9 12h1M14 12h1M9 16h1M14 16h1" />
1200
+ ${name === 'building-check' ? html`<path d="m14.5 18.5 1.5 1.5 3-3" />` : nothing}
1201
+ ${name === 'building-plus' ? html`<path d="M17 15v5M14.5 17.5h5" />` : nothing}
1202
+ </svg>`
1203
+ }
1204
+ if (name === 'users') {
1205
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true">
1206
+ <circle cx="9" cy="8" r="3" />
1207
+ <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" />
1208
+ </svg>`
1209
+ }
1210
+ if (name === 'alert') {
1211
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true">
1212
+ <path d="M12 3 2.8 20h18.4zM12 9v4M12 17h.01" />
1213
+ </svg>`
1214
+ }
1215
+ return html`<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4 10-10" /></svg>`
1216
+ }