批量优化

This commit is contained in:
hero920103 committed 2026-10-03 23:46:55 +08:00
1 parent e3db229f41
commit aa92ca40a2
79 files changed
+7495 -1104

No files matched your search

+13 -2
View File
@@ -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;
}
+232
View File
@@ -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>