|
@@ -1,69 +1,98 @@
|
|
|
-import { View, Text } from '@tarojs/components';
|
|
|
|
|
|
|
+import { View, Text, Input as TaroInput } from '@tarojs/components';
|
|
|
import Taro, { useLoad } from '@tarojs/taro';
|
|
import Taro, { useLoad } from '@tarojs/taro';
|
|
|
-import { useCallback, useState } from 'react';
|
|
|
|
|
|
|
+import { useCallback, useState, useRef } from 'react';
|
|
|
import { Button, Checkbox, Form, Input, Dialog } from '@nutui/nutui-react-taro';
|
|
import { Button, Checkbox, Form, Input, Dialog } from '@nutui/nutui-react-taro';
|
|
|
import UserAgreement from '@/components/UserAgreement';
|
|
import UserAgreement from '@/components/UserAgreement';
|
|
|
-import { CloudIcon, PasswordIcon, UserIcon } from '@/components/Icon';
|
|
|
|
|
|
|
+import { UserSmileIcon } from '@/components/Icon';
|
|
|
import { useLoginStore } from '@/stores/login';
|
|
import { useLoginStore } from '@/stores/login';
|
|
|
-import { LoginFormData } from '@/schemas/login';
|
|
|
|
|
|
|
+import { sendSmsCodeApi } from '@/services/apis';
|
|
|
|
|
+import { isValidMobile } from '@/schemas/login';
|
|
|
|
|
|
|
|
import './index.less';
|
|
import './index.less';
|
|
|
import { useUserStore } from '@/stores/user';
|
|
import { useUserStore } from '@/stores/user';
|
|
|
|
|
|
|
|
export default function Login() {
|
|
export default function Login() {
|
|
|
- const { formData, updateField, validate } = useLoginStore();
|
|
|
|
|
|
|
+ const { formData, updateField, countdown, startCountdown } = useLoginStore();
|
|
|
|
|
|
|
|
const [showDialog, setShowDialog] = useState(false);
|
|
const [showDialog, setShowDialog] = useState(false);
|
|
|
const [agreed, setAgreed] = useState(false);
|
|
const [agreed, setAgreed] = useState(false);
|
|
|
const [loading, setLoading] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
+ const [sending, setSending] = useState(false);
|
|
|
|
|
|
|
|
useLoad(() => {
|
|
useLoad(() => {
|
|
|
console.log('Login page loaded.');
|
|
console.log('Login page loaded.');
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const handleInputChange = useCallback(
|
|
|
|
|
- (key: keyof LoginFormData) => (value: any) => {
|
|
|
|
|
- if (typeof value === 'string') {
|
|
|
|
|
- updateField(key, value);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (value && typeof value === 'object') {
|
|
|
|
|
- const next =
|
|
|
|
|
- (value.detail && value.detail.value) || (value.target && value.target.value) || '';
|
|
|
|
|
- updateField(key, String(next));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const getInputValue = (value: any): string => {
|
|
|
|
|
+ if (typeof value === 'string') return value;
|
|
|
|
|
+ if (value && typeof value === 'object') {
|
|
|
|
|
+ if (value.detail && value.detail.value) return value.detail.value;
|
|
|
|
|
+ if (value.target && value.target.value) return value.target.value;
|
|
|
|
|
+ }
|
|
|
|
|
+ return '';
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleMobileChange = useCallback(
|
|
|
|
|
+ (value: any) => {
|
|
|
|
|
+ const v = getInputValue(value).replace(/\D/g, '').slice(0, 11);
|
|
|
|
|
+ updateField('mobile', v);
|
|
|
|
|
+ },
|
|
|
|
|
+ [updateField],
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const handleCodeChange = useCallback(
|
|
|
|
|
+ (value: any) => {
|
|
|
|
|
+ const v = getInputValue(value).replace(/\D/g, '').slice(0, 6);
|
|
|
|
|
+ updateField('code', v);
|
|
|
},
|
|
},
|
|
|
[updateField],
|
|
[updateField],
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+ const handleSendCode = async () => {
|
|
|
|
|
+ if (!isValidMobile(formData.mobile)) {
|
|
|
|
|
+ Taro.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ setSending(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await sendSmsCodeApi(formData.mobile);
|
|
|
|
|
+ startCountdown();
|
|
|
|
|
+ Taro.showToast({ title: '验证码已发送', icon: 'success' });
|
|
|
|
|
+ } catch (error: unknown) {
|
|
|
|
|
+ console.error('发送验证码失败:', error);
|
|
|
|
|
+ Taro.showToast({ title: (error as Error).message || '发送失败', icon: 'none' });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setSending(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleLogin = async (nextAgreed: boolean = agreed) => {
|
|
const handleLogin = async (nextAgreed: boolean = agreed) => {
|
|
|
- if (validate()) {
|
|
|
|
|
- const agreedValue = nextAgreed;
|
|
|
|
|
- console.log('Login button clicked. Agreed:', agreedValue);
|
|
|
|
|
-
|
|
|
|
|
- if (!agreedValue) {
|
|
|
|
|
- setShowDialog(true);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- console.log('Login button clicked. Logging in...');
|
|
|
|
|
-
|
|
|
|
|
- try {
|
|
|
|
|
- setLoading(true);
|
|
|
|
|
- await useUserStore.getState().login(formData);
|
|
|
|
|
- Taro.showToast({ title: '登录成功', icon: 'success', duration: 2000 });
|
|
|
|
|
- Taro.switchTab({ url: '/pages/index/index' });
|
|
|
|
|
- } catch (error: unknown) {
|
|
|
|
|
- Taro.showToast({ title: (error as Error).message || '登录失败', icon: 'error', duration: 2000 });
|
|
|
|
|
- } finally {
|
|
|
|
|
- setLoading(false);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- } else {
|
|
|
|
|
- const firstError = Object.values(useLoginStore.getState().errors)[0];
|
|
|
|
|
- if (firstError) {
|
|
|
|
|
- Taro.showToast({ title: firstError, icon: 'none', duration: 2000 });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const { formData: fd, errors } = useLoginStore.getState();
|
|
|
|
|
+
|
|
|
|
|
+ if (!fd.mobile || fd.mobile.length < 11) {
|
|
|
|
|
+ Taro.showToast({ title: '请输入正确的手机号', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!fd.code || fd.code.length < 4) {
|
|
|
|
|
+ Taro.showToast({ title: '请输入验证码', icon: 'none' });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const agreedValue = nextAgreed;
|
|
|
|
|
+ if (!agreedValue) {
|
|
|
|
|
+ setShowDialog(true);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ await useUserStore.getState().login(fd);
|
|
|
|
|
+ Taro.showToast({ title: '登录成功', icon: 'success', duration: 2000 });
|
|
|
|
|
+ Taro.switchTab({ url: '/pages/index/index' });
|
|
|
|
|
+ } catch (error: unknown) {
|
|
|
|
|
+ Taro.showToast({ title: (error as Error).message || '登录失败', icon: 'error', duration: 2000 });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -85,60 +114,66 @@ export default function Login() {
|
|
|
</View>
|
|
</View>
|
|
|
|
|
|
|
|
<View className="login-card">
|
|
<View className="login-card">
|
|
|
- <Form
|
|
|
|
|
- divider
|
|
|
|
|
- labelPosition="left"
|
|
|
|
|
- starPosition="right"
|
|
|
|
|
- style={{ '--nutui-form-item-label-width': '30px' } as any}
|
|
|
|
|
- >
|
|
|
|
|
- <Form.Item label={<CloudIcon />} name="systemId">
|
|
|
|
|
- <Input
|
|
|
|
|
- value={formData.systemId}
|
|
|
|
|
- onChange={handleInputChange('systemId')}
|
|
|
|
|
- placeholder="请输入系统号"
|
|
|
|
|
- clearable
|
|
|
|
|
- className="login-input"
|
|
|
|
|
- disabled={loading}
|
|
|
|
|
- />
|
|
|
|
|
- </Form.Item>
|
|
|
|
|
-
|
|
|
|
|
- <Form.Item label={<UserIcon />} name="username">
|
|
|
|
|
- <Input
|
|
|
|
|
- value={formData.username}
|
|
|
|
|
- onChange={handleInputChange('username')}
|
|
|
|
|
- placeholder="请输入用户名"
|
|
|
|
|
- clearable
|
|
|
|
|
- className="login-input"
|
|
|
|
|
|
|
+ {/* 手机号输入 */}
|
|
|
|
|
+ <View className="form-item">
|
|
|
|
|
+ <View className="form-item-label">手机号</View>
|
|
|
|
|
+ <View className="mobile-input-row">
|
|
|
|
|
+ <View className="phone-prefix">+86</View>
|
|
|
|
|
+ <TaroInput
|
|
|
|
|
+ className="mobile-input"
|
|
|
|
|
+ value={formData.mobile}
|
|
|
|
|
+ onInput={handleMobileChange}
|
|
|
|
|
+ placeholder="请输入手机号"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ maxlength={11}
|
|
|
disabled={loading}
|
|
disabled={loading}
|
|
|
/>
|
|
/>
|
|
|
- </Form.Item>
|
|
|
|
|
-
|
|
|
|
|
- <Form.Item label={<PasswordIcon />} name="password">
|
|
|
|
|
- <Input
|
|
|
|
|
- value={formData.password}
|
|
|
|
|
- onChange={handleInputChange('password')}
|
|
|
|
|
- placeholder="请输入密码"
|
|
|
|
|
- type="password"
|
|
|
|
|
- clearable
|
|
|
|
|
- className="login-input"
|
|
|
|
|
|
|
+ </View>
|
|
|
|
|
+ </View>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 验证码输入 */}
|
|
|
|
|
+ <View className="form-item">
|
|
|
|
|
+ <View className="form-item-label">验证码</View>
|
|
|
|
|
+ <View className="code-input-row">
|
|
|
|
|
+ <TaroInput
|
|
|
|
|
+ className="code-input"
|
|
|
|
|
+ value={formData.code}
|
|
|
|
|
+ onInput={handleCodeChange}
|
|
|
|
|
+ placeholder="请输入验证码"
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ maxlength={6}
|
|
|
disabled={loading}
|
|
disabled={loading}
|
|
|
/>
|
|
/>
|
|
|
- </Form.Item>
|
|
|
|
|
- </Form>
|
|
|
|
|
-
|
|
|
|
|
- <View className="login-button">
|
|
|
|
|
- <Button block type="primary" size="large" onClick={() => handleLogin()} disabled={loading}>
|
|
|
|
|
- {loading ? '登录中...' : '登录'}
|
|
|
|
|
- </Button>
|
|
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="send-code-btn"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ type={countdown > 0 ? 'default' : 'primary'}
|
|
|
|
|
+ disabled={countdown > 0 || sending}
|
|
|
|
|
+ onClick={handleSendCode}
|
|
|
|
|
+ >
|
|
|
|
|
+ {countdown > 0 ? `${countdown}s` : sending ? '发送中...' : '获取验证码'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </View>
|
|
|
</View>
|
|
</View>
|
|
|
|
|
|
|
|
|
|
+ <Button
|
|
|
|
|
+ className="login-button"
|
|
|
|
|
+ block
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ onClick={() => handleLogin()}
|
|
|
|
|
+ disabled={loading}
|
|
|
|
|
+ >
|
|
|
|
|
+ {loading ? '登录中...' : '登录'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+
|
|
|
<View className="login-agreement">
|
|
<View className="login-agreement">
|
|
|
<Checkbox
|
|
<Checkbox
|
|
|
checked={agreed}
|
|
checked={agreed}
|
|
|
onChange={setAgreed}
|
|
onChange={setAgreed}
|
|
|
label={
|
|
label={
|
|
|
<Text>
|
|
<Text>
|
|
|
- 我已经
|
|
|
|
|
|
|
+ 我已阅读并同意
|
|
|
<UserAgreement />
|
|
<UserAgreement />
|
|
|
</Text>
|
|
</Text>
|
|
|
}
|
|
}
|