批量优化
This commit is contained in:
1 parent
e3db229f41
commit
aa92ca40a2
79 files changed
+7495
-1104
No files matched your search
@@ -148,6 +148,13 @@ const staticRoutes = [
|
||||
component: () => import("@/views/auth/mpLogin.vue"),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
// 移动端免登录页(uniapp APP / 小程序带一次性票据 → 换发 Web 端令牌,避免二次登录)
|
||||
{
|
||||
path: "/app-login",
|
||||
name: "AppLogin",
|
||||
component: () => import("@/views/auth/appLogin.vue"),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
name: "Home",
|
||||
@@ -317,8 +324,12 @@ async function gotoAuthorize() {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const token = localStorage.getItem("token");
|
||||
|
||||
// 认证中心回跳页 / 微信登录兑换页:始终放行,由页面自身完成令牌兑换
|
||||
if (to.path === "/auth/callback" || to.path === "/mp-login") {
|
||||
// 认证中心回跳页 / 微信登录兑换页 / 移动端免登录页:始终放行,由页面自身完成令牌兑换
|
||||
if (
|
||||
to.path === "/auth/callback" ||
|
||||
to.path === "/mp-login" ||
|
||||
to.path === "/app-login"
|
||||
) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="mp-login">
|
||||
<div class="box">
|
||||
<!-- 企业选择(一人多企业) -->
|
||||
<template v-if="choosing">
|
||||
<p class="title">选择要进入的企业</p>
|
||||
<ul class="tenant-list">
|
||||
<li v-for="t in tenants" :key="t.tid" class="tenant-item" @click="chooseTenant(t)">
|
||||
<span class="t-name">{{ t.tenant_name }}</span>
|
||||
<span v-if="t.name" class="t-meta">{{ t.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-if="chooseErr" class="msg err">{{ chooseErr }}</p>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div v-if="error" class="error">
|
||||
<p class="title">免登录失败</p>
|
||||
<p class="msg">{{ error }}</p>
|
||||
<button class="btn" @click="goHome">返回首页</button>
|
||||
</div>
|
||||
<div v-else class="loading">
|
||||
<div class="spinner"></div>
|
||||
<p>正在同步登录状态,请稍候…</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import request from "@/utils/request";
|
||||
import { setTokens, fetchUserInfo, resolveTenantName } from "@/utils/authClient";
|
||||
|
||||
/**
|
||||
* 移动端免登录兑换页(/app-login)
|
||||
*
|
||||
* 移动端(uniapp APP / 小程序)已用统一认证登录,打开 Web 端功能页时
|
||||
* 携带一次性票据跳转到本页,本页用票据向认证中心换发 Web 端令牌,
|
||||
* 写入本地登录态后跳转到目标页面 —— 全程不再弹出统一登录页。
|
||||
*
|
||||
* 票据一次性、60 秒有效。多企业账号会先展示企业选择界面。
|
||||
*/
|
||||
const error = ref("");
|
||||
const choosing = ref(false);
|
||||
const tenants = ref([]);
|
||||
const chooseErr = ref("");
|
||||
const target = ref("/home");
|
||||
|
||||
onMounted(finish);
|
||||
|
||||
function readQuery() {
|
||||
// hash 路由:参数位于 location.hash 之后,必须整段截取 ? 之后,
|
||||
// 不能用 route.query(与 mpLogin.vue / callback.vue 保持一致)
|
||||
const raw = window.location.href;
|
||||
const queryStr = raw.includes("?") ? raw.slice(raw.indexOf("?") + 1) : "";
|
||||
return Object.fromEntries(new URLSearchParams(queryStr));
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
const query = readQuery();
|
||||
|
||||
// 兑换成功后要回跳的目标页(移动端传入的菜单 path)
|
||||
const redirect = (query.redirect || "").trim();
|
||||
if (redirect) target.value = redirect.startsWith("/") ? redirect : `/${redirect}`;
|
||||
|
||||
const ticket = (query.ticket || query.token || "").trim();
|
||||
if (!ticket) {
|
||||
error.value = "缺少登录票据,请回到移动端重新进入";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request({
|
||||
url: "/auth/app-ticket/exchange",
|
||||
method: "post",
|
||||
data: { ticket, client_id: "yz-backend" },
|
||||
});
|
||||
if (data?.need_choose_tenant) {
|
||||
tenants.value = data.tenants || [];
|
||||
choosing.value = true;
|
||||
return;
|
||||
}
|
||||
await finishLogin(data?.tokens);
|
||||
} catch (e) {
|
||||
error.value = e?.message || "免登录失败,请回到移动端重新进入";
|
||||
}
|
||||
}
|
||||
|
||||
async function chooseTenant(t) {
|
||||
chooseErr.value = "";
|
||||
try {
|
||||
const res = await request({
|
||||
url: "/auth/switch-tenant",
|
||||
method: "post",
|
||||
data: { tid: t.tid, client_id: "yz-backend" },
|
||||
});
|
||||
const tokens = res?.data?.tokens || res?.tokens;
|
||||
await finishLogin(tokens);
|
||||
} catch (e) {
|
||||
chooseErr.value = e?.message || "进入企业失败,请重试";
|
||||
}
|
||||
}
|
||||
|
||||
async function finishLogin(tokens) {
|
||||
setTokens(tokens);
|
||||
await saveUserInfo();
|
||||
// 与 /auth/callback 一致:replaceState 改写 hash 后整页重载,
|
||||
// 避免旧令牌请求 401(先赋值 location.href 会在旧会话状态下先渲染一次并发出请求)
|
||||
window.history.replaceState(
|
||||
{},
|
||||
document.title,
|
||||
`${window.location.pathname}#${target.value}`
|
||||
);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
/** 写入本地用户信息(与 auth/callback.vue 保持一致口径) */
|
||||
async function saveUserInfo() {
|
||||
try {
|
||||
const info = await fetchUserInfo();
|
||||
const userInfo = {
|
||||
id: info.id || info.sub || "",
|
||||
account: info.account || info.mobile || "",
|
||||
name: info.name || info.nickname || "",
|
||||
group_id: info.group_id || "",
|
||||
tid: info.tid || "",
|
||||
tenant_name: resolveTenantName(info),
|
||||
avatar: info.avatar || "",
|
||||
type: "backend",
|
||||
};
|
||||
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
||||
} catch (e) {
|
||||
console.error("获取用户信息失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
window.history.replaceState({}, document.title, `${window.location.pathname}#/home`);
|
||||
window.location.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mp-login {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.box {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 40px 36px;
|
||||
width: 360px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin: 0 auto 16px;
|
||||
border: 3px solid #e4e7ee;
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.msg {
|
||||
font-size: 13px;
|
||||
color: #5a6072;
|
||||
margin-bottom: 20px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.msg.err {
|
||||
color: #cf1322;
|
||||
}
|
||||
.tenant-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
.tenant-item {
|
||||
border: 1px solid #e3e9f5;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tenant-item:hover {
|
||||
border-color: #3973ff;
|
||||
background: #f5f9ff;
|
||||
}
|
||||
.t-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
.t-meta {
|
||||
font-size: 12px;
|
||||
color: #8a94ad;
|
||||
}
|
||||
.btn {
|
||||
height: 40px;
|
||||
padding: 0 24px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user