@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,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
|
+
}
|