|
@@ -2,68 +2,45 @@
|
|
|
<view class="container">
|
|
<view class="container">
|
|
|
<view class="bg-shape shadow"></view>
|
|
<view class="bg-shape shadow"></view>
|
|
|
<view class="bg-shape2 shadow"></view>
|
|
<view class="bg-shape2 shadow"></view>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<view class="login-box">
|
|
<view class="login-box">
|
|
|
<view class="header">
|
|
<view class="header">
|
|
|
<text class="title">欢迎登录</text>
|
|
<text class="title">欢迎登录</text>
|
|
|
<text class="subtitle">登录科研钉钉平台</text>
|
|
<text class="subtitle">登录科研钉钉平台</text>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<view class="form-group">
|
|
<view class="form-group">
|
|
|
<text class="label">账号</text>
|
|
<text class="label">账号</text>
|
|
|
- <uv-input
|
|
|
|
|
- v-model="loginForm.userName"
|
|
|
|
|
- placeholder="请输入您的账号"
|
|
|
|
|
- placeholderClass="placeholder-style"
|
|
|
|
|
- border="none"
|
|
|
|
|
- shape="circle"
|
|
|
|
|
- clearable
|
|
|
|
|
- customStyle="background: #f1f5f9; padding: 20rpx 30rpx; height: 60rpx; border: 2rpx solid transparent; transition: all 0.3s ease; border-radius: 20rpx;"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <uv-input v-model="loginForm.userName" placeholder="请输入您的账号" placeholderClass="placeholder-style" border="none"
|
|
|
|
|
+ shape="circle" clearable
|
|
|
|
|
+ customStyle="background: #f1f5f9; padding: 20rpx 30rpx; height: 60rpx; border: 2rpx solid transparent; transition: all 0.3s ease; border-radius: 20rpx;">
|
|
|
<template #prefix>
|
|
<template #prefix>
|
|
|
<uv-icon name="account" size="22" color="#94a3b8" customStyle="margin-right: 16rpx"></uv-icon>
|
|
<uv-icon name="account" size="22" color="#94a3b8" customStyle="margin-right: 16rpx"></uv-icon>
|
|
|
</template>
|
|
</template>
|
|
|
</uv-input>
|
|
</uv-input>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<view class="form-group">
|
|
<view class="form-group">
|
|
|
<text class="label">密码</text>
|
|
<text class="label">密码</text>
|
|
|
- <uv-input
|
|
|
|
|
- v-model="loginForm.password"
|
|
|
|
|
- :type="showPassword ? 'text' : 'password'"
|
|
|
|
|
- placeholder="请输入密码"
|
|
|
|
|
- placeholderClass="placeholder-style"
|
|
|
|
|
- border="none"
|
|
|
|
|
- shape="circle"
|
|
|
|
|
- customStyle="background: #f1f5f9; padding: 20rpx 30rpx; height: 60rpx; border: 2rpx solid transparent; transition: all 0.3s ease; border-radius: 20rpx;"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <uv-input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" placeholder="请输入密码"
|
|
|
|
|
+ placeholderClass="placeholder-style" border="none" shape="circle"
|
|
|
|
|
+ customStyle="background: #f1f5f9; padding: 20rpx 30rpx; height: 60rpx; border: 2rpx solid transparent; transition: all 0.3s ease; border-radius: 20rpx;">
|
|
|
<template #prefix>
|
|
<template #prefix>
|
|
|
<uv-icon name="lock" size="22" color="#94a3b8" customStyle="margin-right: 16rpx"></uv-icon>
|
|
<uv-icon name="lock" size="22" color="#94a3b8" customStyle="margin-right: 16rpx"></uv-icon>
|
|
|
</template>
|
|
</template>
|
|
|
<template #suffix>
|
|
<template #suffix>
|
|
|
- <uv-icon
|
|
|
|
|
- :name="showPassword ? 'eye-fill' : 'eye-off'"
|
|
|
|
|
- size="22"
|
|
|
|
|
- color="#94a3b8"
|
|
|
|
|
- @click="showPassword = !showPassword"
|
|
|
|
|
- customStyle="margin-left: 16rpx; cursor: pointer"
|
|
|
|
|
- ></uv-icon>
|
|
|
|
|
|
|
+ <uv-icon :name="showPassword ? 'eye-fill' : 'eye-off'" size="22" color="#94a3b8"
|
|
|
|
|
+ @click="showPassword = !showPassword" customStyle="margin-left: 16rpx; cursor: pointer"></uv-icon>
|
|
|
</template>
|
|
</template>
|
|
|
</uv-input>
|
|
</uv-input>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<view class="form-group" v-if="configSetting.isCaptcha === '10'">
|
|
<view class="form-group" v-if="configSetting.isCaptcha === '10'">
|
|
|
<text class="label">验证码</text>
|
|
<text class="label">验证码</text>
|
|
|
<view class="captcha-group">
|
|
<view class="captcha-group">
|
|
|
- <uv-input
|
|
|
|
|
- v-model="loginForm.idValueC"
|
|
|
|
|
- placeholder="请输入验证码"
|
|
|
|
|
- placeholderClass="placeholder-style"
|
|
|
|
|
- border="none"
|
|
|
|
|
- shape="circle"
|
|
|
|
|
- clearable
|
|
|
|
|
- customStyle="flex: 1; background: #f1f5f9; padding: 20rpx 30rpx; height: 60rpx; border: 2rpx solid transparent; transition: all 0.3s ease; border-radius: 20rpx;"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <uv-input v-model="loginForm.idValueC" placeholder="请输入验证码" placeholderClass="placeholder-style" border="none"
|
|
|
|
|
+ shape="circle" clearable
|
|
|
|
|
+ customStyle="flex: 1; background: #f1f5f9; padding: 20rpx 30rpx; height: 60rpx; border: 2rpx solid transparent; transition: all 0.3s ease; border-radius: 20rpx;">
|
|
|
<template #prefix>
|
|
<template #prefix>
|
|
|
<uv-icon name="photo" size="22" color="#94a3b8" customStyle="margin-right: 16rpx"></uv-icon>
|
|
<uv-icon name="photo" size="22" color="#94a3b8" customStyle="margin-right: 16rpx"></uv-icon>
|
|
|
</template>
|
|
</template>
|
|
@@ -71,39 +48,37 @@
|
|
|
<image v-if="codeUrl" :src="codeUrl" class="captcha-img" @click="getCaptchaImg" mode="aspectFit"></image>
|
|
<image v-if="codeUrl" :src="codeUrl" class="captcha-img" @click="getCaptchaImg" mode="aspectFit"></image>
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<view class="options">
|
|
<view class="options">
|
|
|
<uv-checkbox-group v-model="rememberMeArr">
|
|
<uv-checkbox-group v-model="rememberMeArr">
|
|
|
- <uv-checkbox
|
|
|
|
|
- label="记住密码"
|
|
|
|
|
- name="remember"
|
|
|
|
|
- shape="square"
|
|
|
|
|
- activeColor="#3b82f6"
|
|
|
|
|
- labelSize="13"
|
|
|
|
|
- iconSize="14"
|
|
|
|
|
- size="16"
|
|
|
|
|
- labelColor="#475569"
|
|
|
|
|
- ></uv-checkbox>
|
|
|
|
|
|
|
+ <uv-checkbox label="记住密码" name="remember" shape="square" activeColor="#3b82f6" labelSize="13" iconSize="14"
|
|
|
|
|
+ size="16" labelColor="#475569"></uv-checkbox>
|
|
|
</uv-checkbox-group>
|
|
</uv-checkbox-group>
|
|
|
|
|
|
|
|
<!-- <text class="forgot">忘记密码?</text> -->
|
|
<!-- <text class="forgot">忘记密码?</text> -->
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
- <uv-button
|
|
|
|
|
- text="登 录"
|
|
|
|
|
- @click="handleLogin"
|
|
|
|
|
- :loading="loading"
|
|
|
|
|
- shape="circle"
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <uv-button text="登 录" @click="handleLogin" :loading="loading" shape="circle"
|
|
|
color="linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)"
|
|
color="linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)"
|
|
|
- customStyle="height: 100rpx; font-size: 32rpx; font-weight: 600; letter-spacing: 2rpx; box-shadow: 0 10rpx 20rpx rgba(37, 99, 235, 0.3); border: none; margin-top: 20rpx;"
|
|
|
|
|
- />
|
|
|
|
|
-
|
|
|
|
|
|
|
+ customStyle="height: 100rpx; font-size: 32rpx; font-weight: 600; letter-spacing: 2rpx; box-shadow: 0 10rpx 20rpx rgba(37, 99, 235, 0.3); border: none; margin-top: 20rpx;" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 钉钉免登按钮 (仅在钉钉环境下显示) -->
|
|
|
|
|
+ <view v-if="isInDingTalk" style="margin-top: 30rpx;">
|
|
|
|
|
+ <uv-button text="钉钉一键登录" @click="handleDingTalkLogin" :loading="dingTalkLoading" shape="circle" plain
|
|
|
|
|
+ color="#0089ff"
|
|
|
|
|
+ customStyle="height: 100rpx; font-size: 30rpx; font-weight: 500; border: 2rpx solid #0089ff; color: #0089ff;">
|
|
|
|
|
+ <template #prefix>
|
|
|
|
|
+ <uv-icon name="dingtalk" size="24" color="#0089ff" customStyle="margin-right: 12rpx"></uv-icon>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </uv-button>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
<!-- <view class="footer">
|
|
<!-- <view class="footer">
|
|
|
<text class="footer-text">还没有账号?</text>
|
|
<text class="footer-text">还没有账号?</text>
|
|
|
<text class="footer-link" @click="goToRegister">立即注册</text>
|
|
<text class="footer-link" @click="goToRegister">立即注册</text>
|
|
|
</view> -->
|
|
</view> -->
|
|
|
</view>
|
|
</view>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<uv-toast ref="toastRef"></uv-toast>
|
|
<uv-toast ref="toastRef"></uv-toast>
|
|
|
</view>
|
|
</view>
|
|
|
</template>
|
|
</template>
|
|
@@ -117,13 +92,15 @@ import { CACHE_KEY } from '@/constants/index';
|
|
|
// @ts-ignore
|
|
// @ts-ignore
|
|
|
import { Local } from '@/utils/storage';
|
|
import { Local } from '@/utils/storage';
|
|
|
import { useLoginApi } from '@/api/system/login';
|
|
import { useLoginApi } from '@/api/system/login';
|
|
|
-import { encryptWithBackendConfig } from '@/utils/aesCrypto';
|
|
|
|
|
|
|
+import { encryptWithBackendConfig } from '@/utils/aesCrypto';
|
|
|
|
|
+import { getDingTalkAuthCode } from '@/utils/dingtalk';
|
|
|
|
|
+import * as dd from 'dingtalk-jsapi';
|
|
|
|
|
|
|
|
const userStore = useUserStore();
|
|
const userStore = useUserStore();
|
|
|
|
|
|
|
|
const { configSetting } = storeToRefs(userStore);
|
|
const { configSetting } = storeToRefs(userStore);
|
|
|
|
|
|
|
|
-const { checkCaptcha, login } = userStore;
|
|
|
|
|
|
|
+const { checkCaptcha, login, dingTalkLogin } = userStore;
|
|
|
|
|
|
|
|
const loginApi = useLoginApi();
|
|
const loginApi = useLoginApi();
|
|
|
|
|
|
|
@@ -137,10 +114,14 @@ const loginForm = reactive<LoginParams>({
|
|
|
|
|
|
|
|
const codeUrl = ref('');
|
|
const codeUrl = ref('');
|
|
|
const loading = ref(false);
|
|
const loading = ref(false);
|
|
|
|
|
+const dingTalkLoading = ref(false);
|
|
|
const showPassword = ref(false);
|
|
const showPassword = ref(false);
|
|
|
const rememberMeArr = ref<string[]>([]);
|
|
const rememberMeArr = ref<string[]>([]);
|
|
|
const toastRef = ref<any>(null);
|
|
const toastRef = ref<any>(null);
|
|
|
|
|
|
|
|
|
|
+// 是否在钉钉环境中
|
|
|
|
|
+const isInDingTalk = ref(dd.env.platform !== 'notInDingTalk');
|
|
|
|
|
+
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
// --- 新增:登录拦截 ---
|
|
// --- 新增:登录拦截 ---
|
|
|
// 如果缓存里已经有 Token,说明当前处于已登录状态
|
|
// 如果缓存里已经有 Token,说明当前处于已登录状态
|
|
@@ -163,7 +144,7 @@ onMounted(async () => {
|
|
|
loginForm.password = user.password;
|
|
loginForm.password = user.password;
|
|
|
rememberMeArr.value = ['remember'];
|
|
rememberMeArr.value = ['remember'];
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
await checkCaptcha();
|
|
await checkCaptcha();
|
|
|
if (configSetting.value.isCaptcha === '10') {
|
|
if (configSetting.value.isCaptcha === '10') {
|
|
|
getCaptchaImg();
|
|
getCaptchaImg();
|
|
@@ -189,14 +170,14 @@ const handleLogin = async () => {
|
|
|
toastRef.value.show({ message: '请输入密码', type: 'error' });
|
|
toastRef.value.show({ message: '请输入密码', type: 'error' });
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (configSetting.value.isCaptcha === '10' && !loginForm.idValueC) {
|
|
if (configSetting.value.isCaptcha === '10' && !loginForm.idValueC) {
|
|
|
toastRef.value.show({ message: '请输入验证码', type: 'error' });
|
|
toastRef.value.show({ message: '请输入验证码', type: 'error' });
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
loading.value = true;
|
|
loading.value = true;
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
// 使用与后端完全匹配的加密函数加密密码
|
|
// 使用与后端完全匹配的加密函数加密密码
|
|
|
const encryptedPassword = encryptWithBackendConfig(loginForm.password);
|
|
const encryptedPassword = encryptWithBackendConfig(loginForm.password);
|
|
@@ -204,7 +185,7 @@ const handleLogin = async () => {
|
|
|
|
|
|
|
|
// 调用 store 处理真实请求和持久化
|
|
// 调用 store 处理真实请求和持久化
|
|
|
await login(loginForm);
|
|
await login(loginForm);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 记住密码逻辑独立于全局token的持久化
|
|
// 记住密码逻辑独立于全局token的持久化
|
|
|
if (rememberMeArr.value.includes('remember')) {
|
|
if (rememberMeArr.value.includes('remember')) {
|
|
|
Local.set(CACHE_KEY.REMEMBER_USER, {
|
|
Local.set(CACHE_KEY.REMEMBER_USER, {
|
|
@@ -214,9 +195,9 @@ const handleLogin = async () => {
|
|
|
} else {
|
|
} else {
|
|
|
Local.remove(CACHE_KEY.REMEMBER_USER);
|
|
Local.remove(CACHE_KEY.REMEMBER_USER);
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
toastRef.value.show({ message: '登录成功', type: 'success' });
|
|
toastRef.value.show({ message: '登录成功', type: 'success' });
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 登录成功跳转首页
|
|
// 登录成功跳转首页
|
|
|
setTimeout(() => {
|
|
setTimeout(() => {
|
|
|
uni.switchTab({ url: '/pages/home/index' }).catch((err) => {
|
|
uni.switchTab({ url: '/pages/home/index' }).catch((err) => {
|
|
@@ -224,7 +205,7 @@ const handleLogin = async () => {
|
|
|
uni.reLaunch({ url: '/pages/home/index' });
|
|
uni.reLaunch({ url: '/pages/home/index' });
|
|
|
});
|
|
});
|
|
|
}, 1000);
|
|
}, 1000);
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('Login Failed', error);
|
|
console.error('Login Failed', error);
|
|
|
// 登录失败若有验证码,刷新验证码
|
|
// 登录失败若有验证码,刷新验证码
|
|
@@ -236,6 +217,46 @@ const handleLogin = async () => {
|
|
|
loading.value = false;
|
|
loading.value = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 钉钉免登逻辑
|
|
|
|
|
+ */
|
|
|
|
|
+const handleDingTalkLogin = async () => {
|
|
|
|
|
+ dingTalkLoading.value = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 尝试从当前 URL 获取 corpId,或者使用配置
|
|
|
|
|
+ // 注意:如果是通过 HBuilderX 运行到浏览器的,通常需要手动传入 corpId 调试
|
|
|
|
|
+ const corpId = uni.getLaunchOptionsSync()?.query?.corpId || import.meta.env.VITE_DINGTALK_CORPID;
|
|
|
|
|
+
|
|
|
|
|
+ const code = await getDingTalkAuthCode(corpId);
|
|
|
|
|
+
|
|
|
|
|
+ if (!code) {
|
|
|
|
|
+ throw new Error('获取授权码失败');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ toastRef.value.show({ message: code, type: 'error' });
|
|
|
|
|
+
|
|
|
|
|
+ await dingTalkLogin(code);
|
|
|
|
|
+
|
|
|
|
|
+ toastRef.value.show({ message: '钉钉免登成功', type: 'success' });
|
|
|
|
|
+
|
|
|
|
|
+ // 跳转首页
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ uni.switchTab({ url: '/pages/home/index' }).catch((err) => {
|
|
|
|
|
+ uni.reLaunch({ url: '/pages/home/index' });
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error('DingTalk Login Error:', error);
|
|
|
|
|
+ toastRef.value.show({
|
|
|
|
|
+ message: error.message || error.errorMessage || '钉钉免登失败',
|
|
|
|
|
+ type: 'error'
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ dingTalkLoading.value = false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
@@ -333,7 +354,8 @@ const handleLogin = async () => {
|
|
|
|
|
|
|
|
.captcha-img {
|
|
.captcha-img {
|
|
|
width: 200rpx;
|
|
width: 200rpx;
|
|
|
- height: 104rpx; /* 与左侧输入框 60(height) + 40(padding) + 4(border) 高度保持一致 */
|
|
|
|
|
|
|
+ height: 104rpx;
|
|
|
|
|
+ /* 与左侧输入框 60(height) + 40(padding) + 4(border) 高度保持一致 */
|
|
|
border-radius: 20rpx;
|
|
border-radius: 20rpx;
|
|
|
background: #f1f5f9;
|
|
background: #f1f5f9;
|
|
|
flex-shrink: 0;
|
|
flex-shrink: 0;
|