@liujitcn/kratos-uni-app-core 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/module.mjs +84 -0
- package/dist/vite.mjs +405 -0
- package/package.json +58 -0
- package/src/api/base/config.ts +20 -0
- package/src/api/base/file.ts +79 -0
- package/src/api/base/login.ts +87 -0
- package/src/api/base/menu.ts +17 -0
- package/src/api/base/oauth.ts +149 -0
- package/src/api/system/auth.ts +54 -0
- package/src/api/system/base_area.ts +34 -0
- package/src/api/system/base_dict.ts +24 -0
- package/src/bootstrap.ts +23 -0
- package/src/components/BootstrapStatus.vue +39 -0
- package/src/components/KratosTabBar.vue +73 -0
- package/src/index.ts +6 -0
- package/src/module.ts +85 -0
- package/src/navigation-pattern.d.mts +2 -0
- package/src/navigation-pattern.mjs +29 -0
- package/src/navigation-tree.mjs +45 -0
- package/src/navigation.ts +330 -0
- package/src/pages.ts +26 -0
- package/src/rpc/base/v1/config.ts +34 -0
- package/src/rpc/base/v1/file.ts +66 -0
- package/src/rpc/base/v1/login.ts +128 -0
- package/src/rpc/base/v1/mcp.ts +20 -0
- package/src/rpc/base/v1/oauth.ts +199 -0
- package/src/rpc/base/v1/sse.ts +22 -0
- package/src/rpc/common/v1/common.ts +71 -0
- package/src/rpc/common/v1/enum.ts +71 -0
- package/src/rpc/common/v1/types.ts +77 -0
- package/src/rpc/google/protobuf/empty.ts +19 -0
- package/src/rpc/google/protobuf/wrappers.ts +124 -0
- package/src/rpc/system/app/v1/auth.ts +54 -0
- package/src/rpc/system/app/v1/base_area.ts +24 -0
- package/src/rpc/system/app/v1/base_dict.ts +37 -0
- package/src/rpc/system/app/v1/base_menu.ts +61 -0
- package/src/rpc/system/common/v1/enum.ts +80 -0
- package/src/static/images/avatar.png +0 -0
- package/src/static/images/center_bg.png +0 -0
- package/src/static/images/logo_icon.png +0 -0
- package/src/static/images/navigator_bg.png +0 -0
- package/src/static/tabs/home_default.png +0 -0
- package/src/static/tabs/home_selected.png +0 -0
- package/src/static/tabs/user_default.png +0 -0
- package/src/static/tabs/user_selected.png +0 -0
- package/src/stores/index.ts +14 -0
- package/src/stores/modules/setting.ts +56 -0
- package/src/stores/modules/user.ts +266 -0
- package/src/styles/base.scss +42 -0
- package/src/types/env.d.ts +24 -0
- package/src/types/go-captcha-uni.d.ts +7 -0
- package/src/types/uni-helper.d.ts +54 -0
- package/src/uni.scss +76 -0
- package/src/utils/auth.ts +67 -0
- package/src/utils/file.ts +95 -0
- package/src/utils/http.ts +430 -0
- package/src/utils/index.ts +79 -0
- package/src/utils/navigation.ts +81 -0
- package/src/utils/passwordCrypto.ts +275 -0
- package/src/views/pages/index/index.vue +227 -0
- package/src/views/pages/login/login.vue +1133 -0
- package/src/views/pages/login/protocal.vue +176 -0
- package/src/views/pages/status/index.vue +36 -0
- package/src/views/pages/webview/webview.vue +136 -0
- package/src/vite.ts +474 -0
|
@@ -0,0 +1,1133 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useSettingStore, useUserStore } from '../../../stores'
|
|
3
|
+
import type { LoginRequest } from '../../../rpc/base/v1/login'
|
|
4
|
+
import { onLoad } from '@dcloudio/uni-app'
|
|
5
|
+
import { computed, defineAsyncComponent, reactive, ref } from 'vue'
|
|
6
|
+
import { defLoginService } from '../../../api/base/login'
|
|
7
|
+
import defaultLogo from '../../../static/images/logo_icon.png'
|
|
8
|
+
import { homeTabPage } from '../../../utils/navigation'
|
|
9
|
+
import { PASSWORD_CRYPTO_SCENE, encryptPassword } from '../../../utils/passwordCrypto'
|
|
10
|
+
import { navigateAppRoute } from '../../../navigation'
|
|
11
|
+
|
|
12
|
+
// go-captcha-uni 仅在 H5 端使用,小程序端不支持 defineAsyncComponent,需按平台裁剪。
|
|
13
|
+
// #ifdef H5
|
|
14
|
+
const GoCaptchaUni = defineAsyncComponent(() => import('go-captcha-uni'))
|
|
15
|
+
// #endif
|
|
16
|
+
|
|
17
|
+
const userStore = useUserStore()
|
|
18
|
+
const settingStore = useSettingStore()
|
|
19
|
+
const wechatMiniProvider = 'wechatmini'
|
|
20
|
+
const loginSettingsReady = ref(false)
|
|
21
|
+
let loginSettingsPromise: Promise<boolean> | undefined
|
|
22
|
+
|
|
23
|
+
const configuredMainTitle = computed(() => settingStore.getData('mainTitle') || '应用框架示例')
|
|
24
|
+
const configuredSubTitle = computed(() => settingStore.getData('subTitle') || '欢迎使用本应用')
|
|
25
|
+
const configuredAppLogo = computed(() => settingStore.getData('appLogo') || defaultLogo)
|
|
26
|
+
const showTenantCode = computed(() => settingStore.getData('showTenantCode') !== 'false')
|
|
27
|
+
|
|
28
|
+
// 是否同意协议
|
|
29
|
+
const isAgreePrivacy = ref(false)
|
|
30
|
+
const isAgreePrivacyShakeY = ref(false)
|
|
31
|
+
const loading = ref(false)
|
|
32
|
+
|
|
33
|
+
const toggleAgreePrivacy = () => {
|
|
34
|
+
isAgreePrivacy.value = !isAgreePrivacy.value
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const triggerAgreePrivacyShake = () => {
|
|
38
|
+
isAgreePrivacyShakeY.value = true
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
isAgreePrivacyShakeY.value = false
|
|
41
|
+
}, 500)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 打开服务条款
|
|
45
|
+
const onOpenServiceProtocol = () => {
|
|
46
|
+
navigateAppRoute('app/protocol/service')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 打开隐私协议
|
|
50
|
+
const onOpenPrivacyContract = () => {
|
|
51
|
+
navigateAppRoute('app/protocol/privacy')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// #ifdef MP-WEIXIN
|
|
55
|
+
const miniBinding = ref(false)
|
|
56
|
+
const miniCaptchaImage = ref('')
|
|
57
|
+
const miniForm = reactive({
|
|
58
|
+
tenant_code: '',
|
|
59
|
+
user_name: '',
|
|
60
|
+
captcha_code: '',
|
|
61
|
+
captcha_id: '',
|
|
62
|
+
})
|
|
63
|
+
const miniPasswordValue = ref('')
|
|
64
|
+
|
|
65
|
+
type MiniFileSystemManager = {
|
|
66
|
+
writeFile(options: {
|
|
67
|
+
filePath: string
|
|
68
|
+
data: string
|
|
69
|
+
encoding: 'base64'
|
|
70
|
+
success: () => void
|
|
71
|
+
fail: () => void
|
|
72
|
+
}): void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type MiniWxRuntime = typeof wx & {
|
|
76
|
+
env: { USER_DATA_PATH: string }
|
|
77
|
+
getFileSystemManager: () => MiniFileSystemManager
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 将接口返回的验证码 Data URI 转为小程序可渲染的临时文件。 */
|
|
81
|
+
const resolveMiniCaptchaImage = (payload: string, captchaId: string) => {
|
|
82
|
+
if (!payload.startsWith('data:image/')) {
|
|
83
|
+
return Promise.resolve(payload)
|
|
84
|
+
}
|
|
85
|
+
const commaIndex = payload.indexOf(',')
|
|
86
|
+
if (commaIndex < 0) {
|
|
87
|
+
return Promise.resolve(payload)
|
|
88
|
+
}
|
|
89
|
+
const wxRuntime = wx as MiniWxRuntime
|
|
90
|
+
// 每次使用不同路径,避免微信小程序复用同一 image src 的缓存。
|
|
91
|
+
const filePath = `${wxRuntime.env.USER_DATA_PATH}/kratos-login-captcha-${captchaId}.png`
|
|
92
|
+
return new Promise<string>((resolve, reject) => {
|
|
93
|
+
wxRuntime.getFileSystemManager().writeFile({
|
|
94
|
+
filePath,
|
|
95
|
+
data: payload.slice(commaIndex + 1),
|
|
96
|
+
encoding: 'base64',
|
|
97
|
+
success: () => resolve(filePath),
|
|
98
|
+
fail: () => reject(new Error('验证码图片写入失败')),
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 加载小程序绑定所需的数字验证码。 */
|
|
104
|
+
const refreshMiniCaptcha = async () => {
|
|
105
|
+
try {
|
|
106
|
+
const captcha = await defLoginService.Captcha({ type: 'digit' })
|
|
107
|
+
miniCaptchaImage.value = await resolveMiniCaptchaImage(
|
|
108
|
+
captcha.captcha_base64,
|
|
109
|
+
captcha.captcha_id,
|
|
110
|
+
)
|
|
111
|
+
miniForm.captcha_id = captcha.captcha_id
|
|
112
|
+
miniForm.captcha_code = ''
|
|
113
|
+
} catch {
|
|
114
|
+
await uni.showToast({ icon: 'none', title: '验证码加载失败' })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 仅将后端明确返回的未绑定错误转为账号绑定流程。 */
|
|
119
|
+
const isWechatUnboundError = (error: unknown) => {
|
|
120
|
+
if (!error || typeof error !== 'object') return false
|
|
121
|
+
const response = error as {
|
|
122
|
+
statusCode?: number
|
|
123
|
+
data?: {
|
|
124
|
+
reason?: string | number
|
|
125
|
+
message?: string
|
|
126
|
+
binding_required?: boolean
|
|
127
|
+
error?: { reason?: string | number; message?: string }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const reason = response.data?.reason ?? response.data?.error?.reason
|
|
131
|
+
const message = response.data?.message ?? response.data?.error?.message
|
|
132
|
+
return (
|
|
133
|
+
response.data?.binding_required === true ||
|
|
134
|
+
String(reason || '') === 'UNAUTHENTICATED' ||
|
|
135
|
+
message === '微信账号未绑定,请先绑定已有账号'
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 微信授权失败时保留请求层提示,避免异步登录异常冒泡到小程序调试器。
|
|
140
|
+
const wxLogin = async () => {
|
|
141
|
+
const isAgreed = await checkedAgreePrivacy()
|
|
142
|
+
if (!isAgreed) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
if (loading.value) {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
loading.value = true
|
|
149
|
+
let loginCode = ''
|
|
150
|
+
try {
|
|
151
|
+
loginCode = (await wx.login()).code
|
|
152
|
+
} catch {
|
|
153
|
+
await uni.showToast({ icon: 'none', title: '微信登录失败,请稍后重试' })
|
|
154
|
+
loading.value = false
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const session = await userStore.createOauthSession({
|
|
159
|
+
provider: wechatMiniProvider,
|
|
160
|
+
code: loginCode,
|
|
161
|
+
})
|
|
162
|
+
if (session.binding_required) {
|
|
163
|
+
miniBinding.value = true
|
|
164
|
+
void refreshMiniCaptcha()
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
await loginSuccess()
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (!isWechatUnboundError(error)) {
|
|
170
|
+
await uni.showToast({ icon: 'none', title: '微信登录失败,请稍后重试' })
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
miniBinding.value = true
|
|
174
|
+
void refreshMiniCaptcha()
|
|
175
|
+
} finally {
|
|
176
|
+
loading.value = false
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 提交已有账号并绑定当前微信。授权码在每次调用后失效,绑定时重新获取。
|
|
181
|
+
const bindMiniAccount = async () => {
|
|
182
|
+
if (loading.value) return
|
|
183
|
+
if (!(await checkedAgreePrivacy())) return
|
|
184
|
+
if (showTenantCode.value && !miniForm.tenant_code) {
|
|
185
|
+
await uni.showToast({ icon: 'none', title: '请输入租户编号' })
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (!miniForm.user_name || !miniPasswordValue.value) {
|
|
189
|
+
await uni.showToast({ icon: 'none', title: '请输入用户名和密码' })
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
if (!miniForm.captcha_id || !miniForm.captcha_code) {
|
|
193
|
+
await uni.showToast({ icon: 'none', title: '请输入验证码' })
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
loading.value = true
|
|
197
|
+
try {
|
|
198
|
+
const password = await encryptPassword(miniPasswordValue.value, PASSWORD_CRYPTO_SCENE.LOGIN)
|
|
199
|
+
const code = (await wx.login()).code
|
|
200
|
+
await userStore.bindOauthSession({
|
|
201
|
+
provider: wechatMiniProvider,
|
|
202
|
+
code,
|
|
203
|
+
tenant_code: showTenantCode.value ? miniForm.tenant_code : '0000',
|
|
204
|
+
user_name: miniForm.user_name,
|
|
205
|
+
password,
|
|
206
|
+
captcha_code: miniForm.captcha_code,
|
|
207
|
+
captcha_id: miniForm.captcha_id,
|
|
208
|
+
})
|
|
209
|
+
await loginSuccess()
|
|
210
|
+
} catch (error) {
|
|
211
|
+
await refreshMiniCaptcha()
|
|
212
|
+
if (error instanceof Error && error.message) {
|
|
213
|
+
await uni.showToast({ icon: 'none', title: error.message })
|
|
214
|
+
}
|
|
215
|
+
} finally {
|
|
216
|
+
loading.value = false
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// #endif
|
|
220
|
+
|
|
221
|
+
// #ifdef H5
|
|
222
|
+
const captcha_base64 = ref() // 验证码图片Base64字符串
|
|
223
|
+
const defaultCaptchaImageWidth = 170
|
|
224
|
+
const captchaImageWidth = ref(`${defaultCaptchaImageWidth}rpx`)
|
|
225
|
+
const behaviorDialogVisible = ref(false)
|
|
226
|
+
const behaviorLoading = ref(false)
|
|
227
|
+
type CaptchaImageLoadDetail = {
|
|
228
|
+
detail: {
|
|
229
|
+
width: number
|
|
230
|
+
height: number
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
type BehaviorCaptchaPayload = {
|
|
234
|
+
type?: string
|
|
235
|
+
image: string
|
|
236
|
+
thumb: string
|
|
237
|
+
width?: number
|
|
238
|
+
height?: number
|
|
239
|
+
thumbX?: number
|
|
240
|
+
thumbY?: number
|
|
241
|
+
thumbWidth?: number
|
|
242
|
+
thumbHeight?: number
|
|
243
|
+
thumbSize?: number
|
|
244
|
+
}
|
|
245
|
+
type CaptchaPoint = {
|
|
246
|
+
x: number
|
|
247
|
+
y: number
|
|
248
|
+
}
|
|
249
|
+
type ClickCaptchaPoint = CaptchaPoint & {
|
|
250
|
+
key?: number
|
|
251
|
+
index?: number
|
|
252
|
+
}
|
|
253
|
+
type BehaviorCaptchaData = {
|
|
254
|
+
image: string
|
|
255
|
+
thumb: string
|
|
256
|
+
thumbX?: number
|
|
257
|
+
thumbY?: number
|
|
258
|
+
thumbWidth?: number
|
|
259
|
+
thumbHeight?: number
|
|
260
|
+
thumbSize?: number
|
|
261
|
+
angle?: number
|
|
262
|
+
}
|
|
263
|
+
const behaviorCaptchaTypeSet = new Set(['slide', 'click', 'rotate'])
|
|
264
|
+
const configuredCaptchaType = computed(() => settingStore.getData('captchaType'))
|
|
265
|
+
const currentCaptchaType = ref('')
|
|
266
|
+
const isBehaviorCaptcha = computed(() => behaviorCaptchaTypeSet.has(currentCaptchaType.value))
|
|
267
|
+
const behaviorCaptchaData = reactive<BehaviorCaptchaData>({
|
|
268
|
+
image: '',
|
|
269
|
+
thumb: '',
|
|
270
|
+
})
|
|
271
|
+
const behaviorCaptchaWindowWidth = uni.getSystemInfoSync().windowWidth || 375
|
|
272
|
+
const behaviorCaptchaImageWidth = Math.round(
|
|
273
|
+
Math.min(320, Math.max(280, behaviorCaptchaWindowWidth * 0.86)),
|
|
274
|
+
)
|
|
275
|
+
const rotateCaptchaImageSize = Math.round(Math.min(300, behaviorCaptchaImageWidth * 0.94))
|
|
276
|
+
const behaviorCaptchaSource = reactive({
|
|
277
|
+
width: 300,
|
|
278
|
+
height: 220,
|
|
279
|
+
thumbWidth: 60,
|
|
280
|
+
thumbHeight: 60,
|
|
281
|
+
thumbSize: 150,
|
|
282
|
+
})
|
|
283
|
+
const behaviorCaptchaImageHeight = computed(() =>
|
|
284
|
+
Math.round(
|
|
285
|
+
(behaviorCaptchaSource.height * behaviorCaptchaImageWidth) / behaviorCaptchaSource.width,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
const behaviorCaptchaScaleX = computed(
|
|
289
|
+
() => behaviorCaptchaImageWidth / behaviorCaptchaSource.width,
|
|
290
|
+
)
|
|
291
|
+
const behaviorCaptchaScaleY = computed(
|
|
292
|
+
() => behaviorCaptchaImageHeight.value / behaviorCaptchaSource.height,
|
|
293
|
+
)
|
|
294
|
+
const rotateCaptchaScale = computed(() => rotateCaptchaImageSize / behaviorCaptchaSource.width)
|
|
295
|
+
|
|
296
|
+
// 行为验证码按展示尺寸渲染,提交前再换算回服务端原始坐标。
|
|
297
|
+
const toDisplayCaptchaX = (value: number) => Math.round(value * behaviorCaptchaScaleX.value)
|
|
298
|
+
const toDisplayCaptchaY = (value: number) => Math.round(value * behaviorCaptchaScaleY.value)
|
|
299
|
+
const toDisplayRotateSize = (value: number) => Math.round(value * rotateCaptchaScale.value)
|
|
300
|
+
const toOriginalCaptchaX = (value: number) =>
|
|
301
|
+
Math.min(
|
|
302
|
+
behaviorCaptchaSource.width,
|
|
303
|
+
Math.max(0, Math.round(value / behaviorCaptchaScaleX.value)),
|
|
304
|
+
)
|
|
305
|
+
const toOriginalCaptchaY = (value: number) =>
|
|
306
|
+
Math.min(
|
|
307
|
+
behaviorCaptchaSource.height,
|
|
308
|
+
Math.max(0, Math.round(value / behaviorCaptchaScaleY.value)),
|
|
309
|
+
)
|
|
310
|
+
const behaviorCaptchaConfig = computed(() => {
|
|
311
|
+
if (currentCaptchaType.value === 'rotate') {
|
|
312
|
+
return {
|
|
313
|
+
width: behaviorCaptchaImageWidth,
|
|
314
|
+
height: rotateCaptchaImageSize,
|
|
315
|
+
size: rotateCaptchaImageSize,
|
|
316
|
+
showTheme: false,
|
|
317
|
+
verticalPadding: 0,
|
|
318
|
+
horizontalPadding: 0,
|
|
319
|
+
iconSize: 20,
|
|
320
|
+
title: '拖动滑块,将内圈图片转正',
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
width: behaviorCaptchaImageWidth,
|
|
325
|
+
height: behaviorCaptchaImageHeight.value,
|
|
326
|
+
thumbWidth: toDisplayCaptchaX(behaviorCaptchaSource.thumbWidth),
|
|
327
|
+
thumbHeight: toDisplayCaptchaY(behaviorCaptchaSource.thumbHeight),
|
|
328
|
+
showTheme: false,
|
|
329
|
+
verticalPadding: 0,
|
|
330
|
+
horizontalPadding: 0,
|
|
331
|
+
buttonText: '确认',
|
|
332
|
+
iconSize: 20,
|
|
333
|
+
dotSize: 20,
|
|
334
|
+
title: currentCaptchaType.value === 'click' ? '请按顺序点击文字' : '请拖动滑块完成拼图',
|
|
335
|
+
}
|
|
336
|
+
})
|
|
337
|
+
const behaviorCaptchaTheme = {
|
|
338
|
+
textColor: '#1f2937',
|
|
339
|
+
iconColor: '#4b5563',
|
|
340
|
+
btnBgColor: '#28bb9c',
|
|
341
|
+
btnBorderColor: '#28bb9c',
|
|
342
|
+
activeColor: '#28bb9c',
|
|
343
|
+
dragBarColor: '#dbe7e3',
|
|
344
|
+
dragBgColor: '#28bb9c',
|
|
345
|
+
dragIconColor: '#ffffff',
|
|
346
|
+
loadingIconColor: '#28bb9c',
|
|
347
|
+
bodyBgColor: '#f1f5f9',
|
|
348
|
+
dotColor: '#ffffff',
|
|
349
|
+
dotBgColor: 'rgba(40, 187, 156, 0.68)',
|
|
350
|
+
dotBorderColor: 'rgba(255, 255, 255, 0.92)',
|
|
351
|
+
}
|
|
352
|
+
// 获取验证码
|
|
353
|
+
const getCaptcha = async () => {
|
|
354
|
+
const requestedCaptchaType = configuredCaptchaType.value
|
|
355
|
+
if (!requestedCaptchaType) {
|
|
356
|
+
throw new Error('登录验证码类型未配置')
|
|
357
|
+
}
|
|
358
|
+
const data = await defLoginService.Captcha({ type: requestedCaptchaType })
|
|
359
|
+
if (!data.type) {
|
|
360
|
+
throw new Error('验证码接口未返回类型')
|
|
361
|
+
}
|
|
362
|
+
currentCaptchaType.value = data.type
|
|
363
|
+
if (!isBehaviorCaptcha.value) {
|
|
364
|
+
behaviorDialogVisible.value = false
|
|
365
|
+
}
|
|
366
|
+
form.value.captcha_id = data.captcha_id
|
|
367
|
+
form.value.captcha_code = ''
|
|
368
|
+
captchaImageWidth.value = `${defaultCaptchaImageWidth}rpx`
|
|
369
|
+
captcha_base64.value = isBehaviorCaptcha.value ? '' : data.captcha_base64
|
|
370
|
+
if (isBehaviorCaptcha.value) {
|
|
371
|
+
applyBehaviorCaptchaPayload(data.captcha_base64)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** 刷新验证码并将接口错误转为页面提示。 */
|
|
376
|
+
const refreshCaptcha = async () => {
|
|
377
|
+
try {
|
|
378
|
+
await getCaptcha()
|
|
379
|
+
return true
|
|
380
|
+
} catch (error) {
|
|
381
|
+
behaviorDialogVisible.value = false
|
|
382
|
+
await uni.showToast({
|
|
383
|
+
icon: 'none',
|
|
384
|
+
title: error instanceof Error ? error.message : '验证码加载失败',
|
|
385
|
+
})
|
|
386
|
+
return false
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 页面加载或普通表单刷新验证码,行为验证码延迟到登录弹窗打开时再请求。
|
|
391
|
+
const loadPageCaptcha = async () => {
|
|
392
|
+
if (isBehaviorCaptcha.value) {
|
|
393
|
+
form.value.captcha_id = ''
|
|
394
|
+
form.value.captcha_code = ''
|
|
395
|
+
captcha_base64.value = ''
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
await getCaptcha()
|
|
399
|
+
}
|
|
400
|
+
// 解析行为验证码图片载荷并映射为官方组件数据。
|
|
401
|
+
const applyBehaviorCaptchaPayload = (payloadText: string) => {
|
|
402
|
+
const payload = JSON.parse(payloadText || '{}') as BehaviorCaptchaPayload
|
|
403
|
+
const payloadType = payload.type || currentCaptchaType.value
|
|
404
|
+
behaviorCaptchaSource.width = payload.width || 300
|
|
405
|
+
behaviorCaptchaSource.height = payload.height || (payloadType === 'rotate' ? 300 : 220)
|
|
406
|
+
behaviorCaptchaSource.thumbWidth = payload.thumbWidth || (payloadType === 'click' ? 180 : 60)
|
|
407
|
+
behaviorCaptchaSource.thumbHeight = payload.thumbHeight || (payloadType === 'click' ? 48 : 60)
|
|
408
|
+
behaviorCaptchaSource.thumbSize = payload.thumbSize || 150
|
|
409
|
+
behaviorCaptchaData.image = payload.image || ''
|
|
410
|
+
behaviorCaptchaData.thumb = payload.thumb || ''
|
|
411
|
+
behaviorCaptchaData.thumbX = toDisplayCaptchaX(payload.thumbX ?? 0)
|
|
412
|
+
behaviorCaptchaData.thumbY = toDisplayCaptchaY(payload.thumbY ?? 0)
|
|
413
|
+
behaviorCaptchaData.thumbWidth = toDisplayCaptchaX(behaviorCaptchaSource.thumbWidth)
|
|
414
|
+
behaviorCaptchaData.thumbHeight = toDisplayCaptchaY(behaviorCaptchaSource.thumbHeight)
|
|
415
|
+
behaviorCaptchaData.thumbSize = toDisplayRotateSize(behaviorCaptchaSource.thumbSize)
|
|
416
|
+
behaviorCaptchaData.angle = 0
|
|
417
|
+
}
|
|
418
|
+
// 根据验证码图片原始比例更新展示宽度。
|
|
419
|
+
const handleCaptchaImageLoad = (event: Event) => {
|
|
420
|
+
const { width, height } = (event as unknown as CaptchaImageLoadDetail).detail
|
|
421
|
+
if (!width || !height) {
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
// 验证码固定展示高度,宽度按图片比例自适应,避免算术验证码横向内容被裁剪。
|
|
425
|
+
const imageWidth = Math.round((60 * width) / height)
|
|
426
|
+
captchaImageWidth.value = `${Math.min(Math.max(imageWidth, defaultCaptchaImageWidth), 300)}rpx`
|
|
427
|
+
}
|
|
428
|
+
// 传统表单登录。
|
|
429
|
+
const form = ref<LoginRequest>({
|
|
430
|
+
tenant_code: '',
|
|
431
|
+
user_name: '',
|
|
432
|
+
password: undefined,
|
|
433
|
+
captcha_id: '',
|
|
434
|
+
captcha_code: '',
|
|
435
|
+
})
|
|
436
|
+
const passwordValue = ref('')
|
|
437
|
+
// 校验账号密码与协议勾选状态。
|
|
438
|
+
const validateLoginForm = async () => {
|
|
439
|
+
if (!(await ensureLoginSettings())) {
|
|
440
|
+
return false
|
|
441
|
+
}
|
|
442
|
+
if (showTenantCode.value && !form.value.tenant_code) {
|
|
443
|
+
await uni.showToast({
|
|
444
|
+
icon: 'none',
|
|
445
|
+
title: '请输入租户编号',
|
|
446
|
+
})
|
|
447
|
+
return false
|
|
448
|
+
}
|
|
449
|
+
if (!form.value.user_name) {
|
|
450
|
+
await uni.showToast({
|
|
451
|
+
icon: 'none',
|
|
452
|
+
title: '请输入用户名或手机号',
|
|
453
|
+
})
|
|
454
|
+
return false
|
|
455
|
+
}
|
|
456
|
+
if (!passwordValue.value) {
|
|
457
|
+
await uni.showToast({
|
|
458
|
+
icon: 'none',
|
|
459
|
+
title: '请输入密码',
|
|
460
|
+
})
|
|
461
|
+
return false
|
|
462
|
+
}
|
|
463
|
+
if (!isBehaviorCaptcha.value && !form.value.captcha_code) {
|
|
464
|
+
await uni.showToast({
|
|
465
|
+
icon: 'none',
|
|
466
|
+
title: '请输入验证码',
|
|
467
|
+
})
|
|
468
|
+
return false
|
|
469
|
+
}
|
|
470
|
+
return checkedAgreePrivacy()
|
|
471
|
+
}
|
|
472
|
+
// 预校验行为验证码并返回可用于登录的一次性令牌。
|
|
473
|
+
const verifyCaptchaToken = async (captchaCode: string) => {
|
|
474
|
+
const result = await defLoginService.VerifyCaptcha({
|
|
475
|
+
captcha_id: form.value.captcha_id,
|
|
476
|
+
captcha_code: captchaCode,
|
|
477
|
+
})
|
|
478
|
+
return result.captcha_token
|
|
479
|
+
}
|
|
480
|
+
// 执行真正的账号登录流程。
|
|
481
|
+
const submitLogin = async (captchaCode: string) => {
|
|
482
|
+
const password = await encryptPassword(passwordValue.value, PASSWORD_CRYPTO_SCENE.LOGIN)
|
|
483
|
+
return userStore.login({
|
|
484
|
+
...form.value,
|
|
485
|
+
tenant_code: showTenantCode.value ? form.value.tenant_code : '0000',
|
|
486
|
+
password,
|
|
487
|
+
captcha_code: captchaCode,
|
|
488
|
+
})
|
|
489
|
+
}
|
|
490
|
+
// 打开行为验证码弹窗。
|
|
491
|
+
const openBehaviorCaptcha = async () => {
|
|
492
|
+
behaviorDialogVisible.value = true
|
|
493
|
+
behaviorLoading.value = true
|
|
494
|
+
try {
|
|
495
|
+
await refreshCaptcha()
|
|
496
|
+
} finally {
|
|
497
|
+
behaviorLoading.value = false
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// 关闭行为验证码弹窗。
|
|
501
|
+
const closeBehaviorCaptcha = () => {
|
|
502
|
+
behaviorDialogVisible.value = false
|
|
503
|
+
}
|
|
504
|
+
// 验证行为验证码并继续登录。
|
|
505
|
+
const verifyBehaviorCaptcha = async (captchaCode: string, reset: () => void) => {
|
|
506
|
+
if (behaviorLoading.value) {
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
behaviorLoading.value = true
|
|
510
|
+
try {
|
|
511
|
+
const captchaToken = await verifyCaptchaToken(captchaCode)
|
|
512
|
+
behaviorDialogVisible.value = false
|
|
513
|
+
await submitLogin(captchaToken)
|
|
514
|
+
await loginSuccess()
|
|
515
|
+
} catch {
|
|
516
|
+
reset()
|
|
517
|
+
await refreshCaptcha()
|
|
518
|
+
} finally {
|
|
519
|
+
behaviorLoading.value = false
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
// 行为验证码确认回调。
|
|
523
|
+
const onBehaviorConfirm = (
|
|
524
|
+
value: CaptchaPoint | ClickCaptchaPoint[] | number,
|
|
525
|
+
reset: () => void,
|
|
526
|
+
) => {
|
|
527
|
+
if (currentCaptchaType.value === 'click') {
|
|
528
|
+
const dots = value as ClickCaptchaPoint[]
|
|
529
|
+
void verifyBehaviorCaptcha(
|
|
530
|
+
JSON.stringify(
|
|
531
|
+
dots.map((dot) => ({ x: toOriginalCaptchaX(dot.x), y: toOriginalCaptchaY(dot.y) })),
|
|
532
|
+
),
|
|
533
|
+
reset,
|
|
534
|
+
)
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
if (currentCaptchaType.value === 'slide') {
|
|
538
|
+
const point = value as CaptchaPoint
|
|
539
|
+
void verifyBehaviorCaptcha(String(toOriginalCaptchaX(point.x)), reset)
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
void verifyBehaviorCaptcha(String(Math.round(value as number)), reset)
|
|
543
|
+
}
|
|
544
|
+
// 刷新行为验证码。
|
|
545
|
+
const onBehaviorRefresh = () => {
|
|
546
|
+
void refreshCaptcha()
|
|
547
|
+
}
|
|
548
|
+
// 表单提交
|
|
549
|
+
const onSubmit = async () => {
|
|
550
|
+
const valid = await validateLoginForm()
|
|
551
|
+
if (!valid) {
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
if (isBehaviorCaptcha.value) {
|
|
555
|
+
await openBehaviorCaptcha()
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
await submitLogin(form.value.captcha_code)
|
|
560
|
+
await loginSuccess()
|
|
561
|
+
} catch {
|
|
562
|
+
form.value.captcha_code = ''
|
|
563
|
+
await refreshCaptcha()
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// #endif
|
|
567
|
+
const loginSuccess = async () => {
|
|
568
|
+
await userStore.getUserProfile()
|
|
569
|
+
// 成功提示
|
|
570
|
+
await uni.showToast({ icon: 'success', title: '登录成功' })
|
|
571
|
+
setTimeout(() => {
|
|
572
|
+
const lastRoute = uni.getStorageSync('lastRoute') || homeTabPage
|
|
573
|
+
if (lastRoute.startsWith(homeTabPage)) {
|
|
574
|
+
uni.setStorageSync('SwitchTabIndex', true)
|
|
575
|
+
}
|
|
576
|
+
uni.removeStorageSync('lastRoute')
|
|
577
|
+
|
|
578
|
+
uni.reLaunch({ url: lastRoute })
|
|
579
|
+
}, 500)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// 请先阅读并勾选协议
|
|
583
|
+
const checkedAgreePrivacy = async () => {
|
|
584
|
+
if (!(await ensureLoginSettings())) {
|
|
585
|
+
return false
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (!settingStore.getData('serviceProtocol') || !settingStore.getData('privacyProtocol')) {
|
|
589
|
+
await uni.showToast({ icon: 'none', title: '服务条款和隐私协议未配置' })
|
|
590
|
+
return false
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (isAgreePrivacy.value) {
|
|
594
|
+
return true
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
triggerAgreePrivacyShake()
|
|
598
|
+
|
|
599
|
+
return new Promise<boolean>((resolve) => {
|
|
600
|
+
uni.showModal({
|
|
601
|
+
title: '提示',
|
|
602
|
+
content: '请先阅读并勾选协议内容,点击确定后将自动勾选并继续登录',
|
|
603
|
+
confirmText: '确定',
|
|
604
|
+
cancelText: '取消',
|
|
605
|
+
success: ({ confirm }) => {
|
|
606
|
+
if (confirm) {
|
|
607
|
+
isAgreePrivacy.value = true
|
|
608
|
+
resolve(true)
|
|
609
|
+
return
|
|
610
|
+
}
|
|
611
|
+
resolve(false)
|
|
612
|
+
},
|
|
613
|
+
fail: () => resolve(false),
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/** 加载登录所需的移动端配置。 */
|
|
619
|
+
const loadLoginSettings = () => {
|
|
620
|
+
if (loginSettingsReady.value) {
|
|
621
|
+
return Promise.resolve(true)
|
|
622
|
+
}
|
|
623
|
+
if (loginSettingsPromise) {
|
|
624
|
+
return loginSettingsPromise
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
loginSettingsPromise = (async () => {
|
|
628
|
+
try {
|
|
629
|
+
await settingStore.loadData()
|
|
630
|
+
// #ifdef H5
|
|
631
|
+
const captchaType = configuredCaptchaType.value
|
|
632
|
+
if (!captchaType) {
|
|
633
|
+
throw new Error('登录验证码类型未配置')
|
|
634
|
+
}
|
|
635
|
+
currentCaptchaType.value = captchaType
|
|
636
|
+
await loadPageCaptcha()
|
|
637
|
+
// #endif
|
|
638
|
+
// #ifdef MP-WEIXIN
|
|
639
|
+
await refreshMiniCaptcha()
|
|
640
|
+
// #endif
|
|
641
|
+
loginSettingsReady.value = true
|
|
642
|
+
return true
|
|
643
|
+
} catch (error) {
|
|
644
|
+
await uni.showToast({
|
|
645
|
+
icon: 'none',
|
|
646
|
+
title: error instanceof Error ? error.message : '移动端配置加载失败',
|
|
647
|
+
})
|
|
648
|
+
loginSettingsPromise = undefined
|
|
649
|
+
return false
|
|
650
|
+
}
|
|
651
|
+
})()
|
|
652
|
+
|
|
653
|
+
return loginSettingsPromise
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** 确保登录前已完成移动端配置加载。 */
|
|
657
|
+
const ensureLoginSettings = () => loadLoginSettings()
|
|
658
|
+
|
|
659
|
+
// 获取 code 登录凭证
|
|
660
|
+
onLoad(() => {
|
|
661
|
+
void loadLoginSettings()
|
|
662
|
+
})
|
|
663
|
+
</script>
|
|
664
|
+
|
|
665
|
+
<template>
|
|
666
|
+
<view class="login-page">
|
|
667
|
+
<view class="login-hero">
|
|
668
|
+
<view class="login-logo-shell">
|
|
669
|
+
<image :src="configuredAppLogo" />
|
|
670
|
+
</view>
|
|
671
|
+
<view class="login-hero-copy">
|
|
672
|
+
<text class="login-title">{{ configuredMainTitle }}</text>
|
|
673
|
+
<text class="login-subtitle">{{ configuredSubTitle }}</text>
|
|
674
|
+
</view>
|
|
675
|
+
</view>
|
|
676
|
+
<view class="login-panel">
|
|
677
|
+
<view class="login-form">
|
|
678
|
+
<!-- 网页端表单登录 -->
|
|
679
|
+
<!-- #ifdef H5 -->
|
|
680
|
+
<input
|
|
681
|
+
v-if="showTenantCode"
|
|
682
|
+
v-model="form.tenant_code"
|
|
683
|
+
class="login-input"
|
|
684
|
+
type="text"
|
|
685
|
+
confirm-type="next"
|
|
686
|
+
placeholder="请输入租户编号"
|
|
687
|
+
/>
|
|
688
|
+
<input
|
|
689
|
+
v-model="form.user_name"
|
|
690
|
+
class="login-input"
|
|
691
|
+
type="text"
|
|
692
|
+
confirm-type="next"
|
|
693
|
+
placeholder="请输入用户名/手机号码"
|
|
694
|
+
@confirm="onSubmit"
|
|
695
|
+
/>
|
|
696
|
+
<input
|
|
697
|
+
v-model="passwordValue"
|
|
698
|
+
class="login-input"
|
|
699
|
+
type="text"
|
|
700
|
+
password
|
|
701
|
+
confirm-type="done"
|
|
702
|
+
placeholder="请输入密码"
|
|
703
|
+
@confirm="onSubmit"
|
|
704
|
+
/>
|
|
705
|
+
<view v-if="!isBehaviorCaptcha" class="captcha-row">
|
|
706
|
+
<input
|
|
707
|
+
v-model="form.captcha_code"
|
|
708
|
+
class="login-input captcha-input"
|
|
709
|
+
type="text"
|
|
710
|
+
confirm-type="done"
|
|
711
|
+
placeholder="请输入验证码"
|
|
712
|
+
@confirm="onSubmit"
|
|
713
|
+
/>
|
|
714
|
+
<view class="captcha-divider"></view>
|
|
715
|
+
<view class="captcha-trigger" :style="{ width: captchaImageWidth }" @tap="getCaptcha">
|
|
716
|
+
<image
|
|
717
|
+
class="captcha-image"
|
|
718
|
+
:style="{ width: captchaImageWidth }"
|
|
719
|
+
:src="captcha_base64"
|
|
720
|
+
mode="aspectFit"
|
|
721
|
+
@load="handleCaptchaImageLoad"
|
|
722
|
+
/>
|
|
723
|
+
</view>
|
|
724
|
+
</view>
|
|
725
|
+
<button @tap="onSubmit" class="login-button login-button-primary">登录</button>
|
|
726
|
+
<view v-if="behaviorDialogVisible" class="login-behavior-mask">
|
|
727
|
+
<view class="login-behavior-panel">
|
|
728
|
+
<view v-if="behaviorLoading" class="login-behavior-loading">加载中...</view>
|
|
729
|
+
<GoCaptchaUni
|
|
730
|
+
:type="currentCaptchaType"
|
|
731
|
+
:data="behaviorCaptchaData"
|
|
732
|
+
:config="behaviorCaptchaConfig"
|
|
733
|
+
:theme="behaviorCaptchaTheme"
|
|
734
|
+
@event-confirm="onBehaviorConfirm"
|
|
735
|
+
@event-refresh="onBehaviorRefresh"
|
|
736
|
+
@event-close="closeBehaviorCaptcha"
|
|
737
|
+
/>
|
|
738
|
+
</view>
|
|
739
|
+
</view>
|
|
740
|
+
<!-- #endif -->
|
|
741
|
+
|
|
742
|
+
<!-- 小程序端授权登录 -->
|
|
743
|
+
<!-- #ifdef MP-WEIXIN -->
|
|
744
|
+
<button
|
|
745
|
+
v-if="!miniBinding"
|
|
746
|
+
class="login-button login-button-primary"
|
|
747
|
+
:loading="loading"
|
|
748
|
+
@tap="wxLogin"
|
|
749
|
+
>
|
|
750
|
+
<text class="icon icon-phone"></text>
|
|
751
|
+
微信一键登录
|
|
752
|
+
</button>
|
|
753
|
+
<view v-else class="login-form">
|
|
754
|
+
<view class="login-bind-tip">该微信尚未绑定,请登录已有账号完成绑定</view>
|
|
755
|
+
<input
|
|
756
|
+
v-if="showTenantCode"
|
|
757
|
+
v-model="miniForm.tenant_code"
|
|
758
|
+
class="login-input"
|
|
759
|
+
type="text"
|
|
760
|
+
placeholder="请输入租户编号"
|
|
761
|
+
/>
|
|
762
|
+
<input
|
|
763
|
+
v-model="miniForm.user_name"
|
|
764
|
+
class="login-input"
|
|
765
|
+
type="text"
|
|
766
|
+
placeholder="请输入用户名/手机号码"
|
|
767
|
+
/>
|
|
768
|
+
<input
|
|
769
|
+
v-model="miniPasswordValue"
|
|
770
|
+
class="login-input"
|
|
771
|
+
type="text"
|
|
772
|
+
password
|
|
773
|
+
placeholder="请输入密码"
|
|
774
|
+
/>
|
|
775
|
+
<view class="captcha-row">
|
|
776
|
+
<input
|
|
777
|
+
v-model="miniForm.captcha_code"
|
|
778
|
+
class="login-input captcha-input"
|
|
779
|
+
type="text"
|
|
780
|
+
placeholder="请输入验证码"
|
|
781
|
+
/>
|
|
782
|
+
<view class="captcha-divider"></view>
|
|
783
|
+
<view class="captcha-trigger" @tap="refreshMiniCaptcha">
|
|
784
|
+
<image class="captcha-image" :src="miniCaptchaImage" mode="aspectFit" />
|
|
785
|
+
</view>
|
|
786
|
+
</view>
|
|
787
|
+
<button
|
|
788
|
+
class="login-button login-button-primary"
|
|
789
|
+
:loading="loading"
|
|
790
|
+
@tap="bindMiniAccount"
|
|
791
|
+
>
|
|
792
|
+
登录并绑定微信
|
|
793
|
+
</button>
|
|
794
|
+
</view>
|
|
795
|
+
<!-- #endif -->
|
|
796
|
+
</view>
|
|
797
|
+
<view class="login-tips" :class="{ animate__shakeY: isAgreePrivacyShakeY }">
|
|
798
|
+
<view class="login-agreement" @tap="toggleAgreePrivacy">
|
|
799
|
+
<view class="login-agree-icon" :class="{ checked: isAgreePrivacy }"></view>
|
|
800
|
+
<text class="login-agree-desc">我已阅读并同意</text>
|
|
801
|
+
<text class="login-agree-link" @tap.stop="onOpenServiceProtocol">《服务条款》</text>
|
|
802
|
+
<text class="login-agree-separator">和</text>
|
|
803
|
+
<text class="login-agree-link" @tap.stop="onOpenPrivacyContract">《隐私协议》</text>
|
|
804
|
+
</view>
|
|
805
|
+
</view>
|
|
806
|
+
</view>
|
|
807
|
+
</view>
|
|
808
|
+
</template>
|
|
809
|
+
|
|
810
|
+
<style lang="scss">
|
|
811
|
+
.login-page {
|
|
812
|
+
display: flex;
|
|
813
|
+
flex-direction: column;
|
|
814
|
+
justify-content: center;
|
|
815
|
+
min-height: 100vh;
|
|
816
|
+
padding: 48rpx 40rpx 56rpx;
|
|
817
|
+
background: linear-gradient(180deg, #f4fbf8 0%, #ffffff 45%, #ffffff 100%);
|
|
818
|
+
box-sizing: border-box;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.login-hero {
|
|
822
|
+
margin-bottom: 40rpx;
|
|
823
|
+
text-align: center;
|
|
824
|
+
|
|
825
|
+
.login-logo-shell {
|
|
826
|
+
display: inline-flex;
|
|
827
|
+
align-items: center;
|
|
828
|
+
justify-content: center;
|
|
829
|
+
width: 180rpx;
|
|
830
|
+
height: 180rpx;
|
|
831
|
+
margin-bottom: 24rpx;
|
|
832
|
+
border-radius: 44rpx;
|
|
833
|
+
background: rgba(255, 255, 255, 0.92);
|
|
834
|
+
box-shadow: 0 18rpx 44rpx rgba(40, 187, 156, 0.12);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
image {
|
|
838
|
+
width: 120rpx;
|
|
839
|
+
height: 120rpx;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.login-hero-copy {
|
|
843
|
+
display: flex;
|
|
844
|
+
flex-direction: column;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
.login-title {
|
|
848
|
+
font-size: 44rpx;
|
|
849
|
+
font-weight: 600;
|
|
850
|
+
color: #1f2937;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.login-subtitle {
|
|
854
|
+
margin-top: 12rpx;
|
|
855
|
+
color: #6b7280;
|
|
856
|
+
font-size: 26rpx;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.login-panel {
|
|
861
|
+
padding: 36rpx 28rpx 30rpx;
|
|
862
|
+
border-radius: 36rpx;
|
|
863
|
+
background: rgba(255, 255, 255, 0.96);
|
|
864
|
+
box-shadow: 0 24rpx 60rpx rgba(15, 23, 42, 0.08);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.login-form {
|
|
868
|
+
display: flex;
|
|
869
|
+
flex-direction: column;
|
|
870
|
+
|
|
871
|
+
.login-bind-tip {
|
|
872
|
+
margin-bottom: 20rpx;
|
|
873
|
+
color: #6b7280;
|
|
874
|
+
font-size: 24rpx;
|
|
875
|
+
line-height: 1.5;
|
|
876
|
+
text-align: center;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.login-input {
|
|
880
|
+
width: 100%;
|
|
881
|
+
height: 88rpx;
|
|
882
|
+
font-size: 28rpx;
|
|
883
|
+
color: #1f2937;
|
|
884
|
+
border-radius: 24rpx;
|
|
885
|
+
border: 1px solid #e5e7eb;
|
|
886
|
+
background: #f9fbfa;
|
|
887
|
+
padding: 0 30rpx;
|
|
888
|
+
margin-bottom: 20rpx;
|
|
889
|
+
box-sizing: border-box;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.login-button {
|
|
893
|
+
display: flex;
|
|
894
|
+
align-items: center;
|
|
895
|
+
justify-content: center;
|
|
896
|
+
width: 100%;
|
|
897
|
+
height: 88rpx;
|
|
898
|
+
margin-top: 12rpx;
|
|
899
|
+
font-size: 30rpx;
|
|
900
|
+
font-weight: 500;
|
|
901
|
+
border-radius: 24rpx;
|
|
902
|
+
color: #fff;
|
|
903
|
+
box-shadow: 0 18rpx 36rpx rgba(40, 187, 156, 0.24);
|
|
904
|
+
|
|
905
|
+
&::after {
|
|
906
|
+
border: 0;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
.icon {
|
|
910
|
+
font-size: 40rpx;
|
|
911
|
+
margin-right: 10rpx;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
.icon-phone::before {
|
|
915
|
+
content: '☎';
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
.login-button-primary {
|
|
920
|
+
background: linear-gradient(135deg, #34c8aa 0%, #28bb9c 100%);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
.captcha-row {
|
|
924
|
+
display: flex;
|
|
925
|
+
align-items: center;
|
|
926
|
+
width: 100%;
|
|
927
|
+
height: 88rpx;
|
|
928
|
+
margin-bottom: 20rpx;
|
|
929
|
+
padding: 0 12rpx 0 30rpx;
|
|
930
|
+
border: 1px solid #e5e7eb;
|
|
931
|
+
border-radius: 24rpx;
|
|
932
|
+
background: #f9fbfa;
|
|
933
|
+
box-sizing: border-box;
|
|
934
|
+
|
|
935
|
+
.captcha-input {
|
|
936
|
+
flex: 1;
|
|
937
|
+
height: 100%;
|
|
938
|
+
margin-bottom: 0;
|
|
939
|
+
padding: 0;
|
|
940
|
+
border: 0;
|
|
941
|
+
background: transparent;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
.captcha-divider {
|
|
945
|
+
width: 1rpx;
|
|
946
|
+
height: 44rpx;
|
|
947
|
+
background: #e7ebf0;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.captcha-trigger {
|
|
951
|
+
display: flex;
|
|
952
|
+
align-items: center;
|
|
953
|
+
justify-content: center;
|
|
954
|
+
flex: 0 0 auto;
|
|
955
|
+
height: 72rpx;
|
|
956
|
+
margin-left: 14rpx;
|
|
957
|
+
border-radius: 18rpx;
|
|
958
|
+
background: #fff;
|
|
959
|
+
box-sizing: border-box;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
.captcha-image {
|
|
963
|
+
width: 170rpx;
|
|
964
|
+
flex-shrink: 0;
|
|
965
|
+
height: 60rpx;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.login-behavior-mask {
|
|
971
|
+
position: fixed;
|
|
972
|
+
z-index: 99;
|
|
973
|
+
top: 0;
|
|
974
|
+
right: 0;
|
|
975
|
+
bottom: 0;
|
|
976
|
+
left: 0;
|
|
977
|
+
display: flex;
|
|
978
|
+
align-items: center;
|
|
979
|
+
justify-content: center;
|
|
980
|
+
padding: 24rpx;
|
|
981
|
+
background: rgba(15, 23, 42, 0.38);
|
|
982
|
+
box-sizing: border-box;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
.login-behavior-panel {
|
|
986
|
+
position: relative;
|
|
987
|
+
width: auto;
|
|
988
|
+
max-width: 100%;
|
|
989
|
+
padding: 24rpx;
|
|
990
|
+
border-radius: 28rpx;
|
|
991
|
+
background: #fff;
|
|
992
|
+
box-shadow: 0 24rpx 70rpx rgba(15, 23, 42, 0.18);
|
|
993
|
+
box-sizing: border-box;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
.login-behavior-loading {
|
|
997
|
+
position: absolute;
|
|
998
|
+
z-index: 2;
|
|
999
|
+
top: 24rpx;
|
|
1000
|
+
right: 24rpx;
|
|
1001
|
+
left: 24rpx;
|
|
1002
|
+
height: 44rpx;
|
|
1003
|
+
font-size: 24rpx;
|
|
1004
|
+
line-height: 44rpx;
|
|
1005
|
+
text-align: center;
|
|
1006
|
+
color: #28bb9c;
|
|
1007
|
+
background: rgba(255, 255, 255, 0.9);
|
|
1008
|
+
border-radius: 999rpx;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
.login-behavior-panel .go-captcha .gc-header {
|
|
1012
|
+
height: auto;
|
|
1013
|
+
min-height: 40rpx;
|
|
1014
|
+
margin-bottom: 12rpx;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
.login-behavior-panel .go-captcha .gc-header .gc-text {
|
|
1018
|
+
font-size: 24rpx;
|
|
1019
|
+
font-weight: 500;
|
|
1020
|
+
line-height: 34rpx;
|
|
1021
|
+
white-space: nowrap;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
.login-behavior-panel .go-captcha .gc-body {
|
|
1025
|
+
margin-top: 0;
|
|
1026
|
+
border-radius: 18rpx;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.login-behavior-panel .go-captcha .gc-footer {
|
|
1030
|
+
padding-top: 22rpx;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.login-behavior-panel .go-captcha .gc-drag-slide-bar {
|
|
1034
|
+
height: 56rpx;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.login-behavior-panel .go-captcha .gc-drag-line {
|
|
1038
|
+
height: 18rpx;
|
|
1039
|
+
background: #dbe7e3;
|
|
1040
|
+
border-radius: 999rpx;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.login-behavior-panel .go-captcha .gc-drag-block {
|
|
1044
|
+
width: 92rpx;
|
|
1045
|
+
height: 56rpx;
|
|
1046
|
+
margin-top: -28rpx;
|
|
1047
|
+
background: linear-gradient(135deg, #36d6b4 0%, #20aa8f 100%);
|
|
1048
|
+
border-radius: 999rpx;
|
|
1049
|
+
box-shadow: 0 14rpx 28rpx rgba(40, 187, 156, 0.28);
|
|
1050
|
+
color: #fff;
|
|
1051
|
+
fill: #fff;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.login-behavior-panel .go-captcha .gc-drag-block.disabled {
|
|
1055
|
+
background: #9adfce;
|
|
1056
|
+
box-shadow: none;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.login-behavior-panel .go-captcha .gc-drag-block .gc-icon {
|
|
1060
|
+
color: #fff;
|
|
1061
|
+
font-size: 42rpx;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.login-behavior-panel .go-captcha .gc-rotate-picture .gc-round {
|
|
1065
|
+
border-width: 8rpx;
|
|
1066
|
+
box-shadow: inset 0 0 0 2rpx rgba(255, 255, 255, 0.78);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.login-behavior-panel .go-captcha .gc-rotate-thumb-block {
|
|
1070
|
+
overflow: hidden;
|
|
1071
|
+
border: 4rpx solid rgba(255, 255, 255, 0.92);
|
|
1072
|
+
border-radius: 50%;
|
|
1073
|
+
box-shadow:
|
|
1074
|
+
0 14rpx 30rpx rgba(15, 23, 42, 0.2),
|
|
1075
|
+
0 0 0 2rpx rgba(15, 23, 42, 0.08);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.login-behavior-panel .go-captcha .gc-rotate-thumb-block .gc-img {
|
|
1079
|
+
border-radius: 50%;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.login-behavior-panel .go-captcha .gc-dots .gc-dot {
|
|
1083
|
+
font-size: 20rpx;
|
|
1084
|
+
font-weight: 600;
|
|
1085
|
+
border-width: 3rpx;
|
|
1086
|
+
box-shadow: 0 6rpx 14rpx rgba(15, 23, 42, 0.18);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.login-tips {
|
|
1090
|
+
margin-top: 26rpx;
|
|
1091
|
+
font-size: 22rpx;
|
|
1092
|
+
color: #999;
|
|
1093
|
+
line-height: 1.6;
|
|
1094
|
+
|
|
1095
|
+
.login-agreement {
|
|
1096
|
+
display: flex;
|
|
1097
|
+
align-items: center;
|
|
1098
|
+
justify-content: center;
|
|
1099
|
+
gap: 6rpx;
|
|
1100
|
+
white-space: nowrap;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.login-agree-separator {
|
|
1104
|
+
white-space: nowrap;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.login-agree-icon {
|
|
1108
|
+
width: 26rpx;
|
|
1109
|
+
height: 26rpx;
|
|
1110
|
+
margin-right: 2rpx;
|
|
1111
|
+
border: 2rpx solid #d1d5db;
|
|
1112
|
+
border-radius: 50%;
|
|
1113
|
+
background-color: #fff;
|
|
1114
|
+
box-sizing: border-box;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.login-agree-icon.checked {
|
|
1118
|
+
border-color: #28bb9c;
|
|
1119
|
+
background-color: #28bb9c;
|
|
1120
|
+
box-shadow: inset 0 0 0 6rpx #fff;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
.login-agree-link {
|
|
1124
|
+
white-space: nowrap;
|
|
1125
|
+
color: #28bb9c;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
.login-agree-desc {
|
|
1129
|
+
color: #9ca3af;
|
|
1130
|
+
white-space: nowrap;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
</style>
|