<template>
|
<view class="login-container">
|
<view v-if="isAutoLogining" class="sso-mask">
|
<uni-load-more
|
status="loading"
|
:content-text="{ contentdown: '自动登录中...' }"
|
></uni-load-more>
|
</view>
|
<view class="header">
|
<image src="/static/avatar/logo.png" class="logo" />
|
<text class="hospital-name">青大附院OPO管理平台</text>
|
</view>
|
<view class="form-container">
|
<view class="input-group">
|
<uni-icons type="contact" size="24" color="#409EFF" />
|
<input v-model="username" placeholder="请输入账号" class="input" />
|
</view>
|
<view class="input-group">
|
<uni-icons type="locked" size="24" color="#409EFF" />
|
<input
|
v-model="password"
|
:password="!showPassword"
|
placeholder="请输入密码"
|
class="input"
|
/>
|
<uni-icons
|
:type="showPassword ? 'eye' : 'eye-slash'"
|
size="22"
|
color="#999"
|
@click="showPassword = !showPassword"
|
/>
|
</view>
|
<button
|
class="login-btn"
|
:class="{ active: username && password }"
|
@click="handleLogin"
|
hover-class="button-hover"
|
>
|
登录
|
</button>
|
</view>
|
</view>
|
</template>
|
|
<script setup>
|
import { ref } from "vue";
|
import { onLoad, onShow } from "@dcloudio/uni-app";
|
import { useUserStore } from "@/stores/user";
|
import { getToken, setToken } from "@/utils/auth";
|
import { encrypt } from "@/utils/crypto";
|
|
let isAutoLogining = false;
|
let hasAutoLogin = false;
|
|
const username = ref("");
|
const password = ref("");
|
const showPassword = ref(false);
|
const redirect = ref("");
|
|
// tabBar 页面列表(与 pages.json 保持一致)
|
const tabBarPages = [
|
"/pages/index/index",
|
"/pages/appointment/index",
|
"/pages/consultation/index",
|
"/pages/my/index",
|
];
|
|
onLoad(async (options) => {
|
console.log("登录页onLoad,原始参数:", options);
|
const launchQuery = uni.getLaunchOptionsSync().query || {};
|
const allParams = { ...launchQuery, ...options };
|
console.log("合并后参数:", allParams);
|
|
// 提取 redirect
|
if (allParams.redirect) {
|
redirect.value = decodeURIComponent(allParams.redirect);
|
}
|
|
// ✅ 优先处理 code 免登
|
const code = allParams.code;
|
if (code) {
|
console.log("检测到免登码,开始解析:", code);
|
// 显示 loading
|
uni.showLoading({ title: "解析免登信息...", mask: true });
|
try {
|
// 调用解析接口
|
const res = await uni.$uapi.get("/GiLink/getCode", { code });
|
uni.hideLoading();
|
console.log(res.data);
|
res.data.passWord = res.data.userPhone;
|
if (!res.data || !res.data.userName || !res.data.passWord) {
|
throw new Error("解析免登码失败");
|
}
|
let obj = JSON.parse(res.data.extContent);
|
// const { userName, passWord, ...otherParams } = res;
|
console.log(res.data.userName);
|
|
const userName = res.data.userName;
|
const passWord = res.data.passWord;
|
const otherParams = {
|
id: obj.infoId,
|
type: obj.type,
|
status: obj.status,
|
fcid: obj.id,
|
};
|
console.log("解析结果:", { userName, otherParams });
|
// 清除可能存在的旧 token
|
if (getToken()) uni.removeStorageSync("token");
|
// 执行 SSO 登录,并将其他业务参数传递下去
|
await handleSSOLogin(userName, passWord, otherParams);
|
} catch (err) {
|
uni.hideLoading();
|
console.error("免登码解析失败:", err);
|
uni.showToast({ title: "免登录失败,请手动登录", icon: "none" });
|
}
|
return;
|
}
|
|
// 原有逻辑:无 code 且已有 token 跳转目标页
|
if (getToken()) {
|
setTimeout(() => navigateToTarget(), 100);
|
return;
|
}
|
|
// 普通登录,清空表单
|
username.value = "";
|
password.value = "";
|
});
|
|
onShow(() => {
|
if (hasAutoLogin) hasAutoLogin = false;
|
});
|
|
/**
|
* SSO 免登
|
*/
|
const handleSSOLogin = async (userName, passWord, pageParams = {}) => {
|
if (isAutoLogining) return;
|
isAutoLogining = true;
|
console.log("开始SSO免登:", { userName, pageParams });
|
|
uni.showLoading({ title: "自动登录中...", mask: true });
|
|
try {
|
const tokenRes = await uni.$uapi.post("/getToken", { userName, passWord });
|
uni.hideLoading();
|
|
if (!tokenRes.data?.token) throw new Error("获取token失败");
|
|
setToken(tokenRes.data.token);
|
|
const userStore = useUserStore();
|
const userInfo = await uni.$uapi.get("/getInfo");
|
if (userInfo && userInfo.user) {
|
userStore.setUserInfo(userInfo.user);
|
if (userInfo.roles) userStore.setroleKey(userInfo.roles);
|
}
|
|
// 确定目标页面
|
let targetPage = redirect.value;
|
if (!targetPage) {
|
const launchOptions = uni.getLaunchOptionsSync();
|
const originalPath = launchOptions.path;
|
if (originalPath && !originalPath.includes("login/Login")) {
|
targetPage = "/" + originalPath;
|
}
|
}
|
if (!targetPage) targetPage = "/pages/index/index";
|
|
hasAutoLogin = true;
|
// 跳转时携带 pageParams(业务参数)
|
await navigateToTargetPage(targetPage, pageParams);
|
} catch (err) {
|
uni.hideLoading();
|
console.error("SSO免登失败:", err);
|
uni.showToast({
|
title: err.message || "自动登录失败,请手动登录",
|
icon: "none",
|
duration: 3000,
|
});
|
uni.removeStorageSync("token");
|
} finally {
|
isAutoLogining = false;
|
}
|
};
|
|
/**
|
* 普通登录
|
*/
|
const handleLogin = async () => {
|
try {
|
if (!username.value || !password.value) {
|
uni.showToast({ title: "请输入账号密码", icon: "none" });
|
return;
|
}
|
uni.showLoading({ title: "登录中...", mask: true });
|
|
const userStore = useUserStore();
|
const encryptedPassword = encrypt(password.value);
|
const encryptedUsername = encrypt(username.value);
|
|
const loginRes = await uni.$uapi.post("/login", {
|
username: encryptedUsername,
|
password: encryptedPassword,
|
});
|
|
if (!loginRes || !loginRes.token) throw new Error("登录失败");
|
setToken(loginRes.token);
|
|
const userInfo = await uni.$uapi.get("/getInfo");
|
if (userInfo && userInfo.user) {
|
userStore.setUserInfo(userInfo.user);
|
|
if (userInfo.roles) userStore.setroleKey(userInfo.roles);
|
}
|
|
uni.hideLoading();
|
await navigateToTarget();
|
} catch (err) {
|
uni.hideLoading();
|
console.error("登录失败:", err);
|
uni.showToast({
|
title: err.message || "登录失败",
|
icon: "none",
|
duration: 3000,
|
});
|
}
|
};
|
|
/**
|
* 根据 redirect 跳转
|
*/
|
const navigateToTarget = () => {
|
if (redirect.value) {
|
return navigateToTargetPage(redirect.value, {});
|
} else {
|
console.log("没有目标页面,跳转首页");
|
uni.switchTab({ url: "/pages/index/index" });
|
return Promise.resolve(true);
|
}
|
};
|
|
/**
|
* 通用跳转函数
|
* @param {string} targetPage 目标页面路径
|
* @param {object} pageParams 附加参数
|
*/
|
const navigateToTargetPage = (targetPage, pageParams = {}) => {
|
return new Promise((resolve) => {
|
if (!targetPage) {
|
console.log("目标页为空,跳转首页");
|
uni.switchTab({ url: "/pages/index/index" });
|
resolve(true);
|
return;
|
}
|
|
let finalUrl = targetPage;
|
const paramKeys = Object.keys(pageParams).filter(
|
(key) => pageParams[key] !== undefined && pageParams[key] !== null,
|
);
|
if (paramKeys.length > 0) {
|
const queryStr = paramKeys
|
.map((key) => `${key}=${encodeURIComponent(pageParams[key])}`)
|
.join("&");
|
finalUrl += (finalUrl.includes("?") ? "&" : "?") + queryStr + "&sso=1";
|
}
|
|
console.log("最终跳转目标URL:", finalUrl);
|
|
const baseUrl = finalUrl.split("?")[0];
|
const isTabBar = tabBarPages.includes(baseUrl);
|
|
setTimeout(() => {
|
if (isTabBar) {
|
uni.switchTab({
|
url: baseUrl,
|
success: () => resolve(true),
|
fail: (err) => {
|
console.error("switchTab失败:", err);
|
uni.switchTab({ url: "/pages/index/index" });
|
resolve(false);
|
},
|
});
|
} else {
|
uni.redirectTo({
|
url: finalUrl,
|
success: () => resolve(true),
|
fail: (err) => {
|
console.error("redirectTo失败:", err);
|
uni.switchTab({ url: "/pages/index/index" });
|
resolve(false);
|
},
|
});
|
}
|
}, 300);
|
});
|
};
|
</script>
|
|
<style scoped>
|
.login-container {
|
padding: 40rpx;
|
background: linear-gradient(to bottom, #e6f7ff, #ffffff);
|
height: 100vh;
|
box-sizing: border-box;
|
position: relative;
|
}
|
|
.sso-mask {
|
position: fixed;
|
top: 0;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
background: rgba(255, 255, 255, 0.9);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
z-index: 9999;
|
}
|
|
.header {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
margin: 100rpx 0 60rpx;
|
}
|
|
.logo {
|
width: 160rpx;
|
height: 160rpx;
|
border-radius: 50%;
|
border: 3rpx solid #d6ecff;
|
box-shadow: 0 6rpx 18rpx rgba(24, 144, 255, 0.3);
|
}
|
|
.hospital-name {
|
font-size: 38rpx;
|
font-weight: bold;
|
color: #1890ff;
|
margin-top: 24rpx;
|
letter-spacing: 3rpx;
|
}
|
|
.form-container {
|
background: #ffffff;
|
border-radius: 24rpx;
|
padding: 50rpx;
|
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.1);
|
}
|
|
.input-group {
|
display: flex;
|
align-items: center;
|
flex-direction: row;
|
padding: 28rpx 0;
|
border-bottom: 1rpx solid #f0f0f0;
|
flex-wrap: nowrap;
|
}
|
|
.input {
|
flex: 1;
|
margin: 0 20rpx;
|
font-size: 30rpx;
|
color: #333;
|
background: transparent;
|
}
|
|
.login-btn {
|
margin-top: 60rpx;
|
height: 90rpx;
|
line-height: 90rpx;
|
border-radius: 50rpx;
|
font-size: 34rpx;
|
border: none;
|
color: white;
|
background: #c0dfff;
|
transition: all 0.3s ease;
|
}
|
|
.login-btn.active {
|
background: linear-gradient(to right, #40a9ff, #1890ff);
|
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.4);
|
}
|
|
.button-hover {
|
opacity: 0.8;
|
}
|
</style>
|