hookehuyr

Merge branch 'feature/授权页面优化' into develop

......@@ -7,13 +7,18 @@
* @Description:
-->
<template>
<router-view></router-view>
<div class="app-shell">
<router-view v-if="app_bootstrap_ready"></router-view>
<div v-else class="app-bootstrap-loading" :style="{ background: styleColor.backgroundColor }">
<van-loading size="24px" vertical :color="styleColor.baseColor">页面授权中...</van-loading>
</div>
</div>
</template>
<script setup>
import { mainStore, useTitle } from "@/utils/generatePackage";
import { computed, watchEffect, onMounted, onUnmounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { mainStore } from "@/utils/generatePackage";
import { computed, ref, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router";
// import { Toast } from "vant";
// 会根据配置判断是否显示调试控件
// eslint-disable-next-line no-unused-vars
......@@ -22,20 +27,21 @@ import vConsole from "@/utils/vconsole";
import wx from 'weixin-js-sdk'
import { wxJsAPI } from '@/api/wx/config'
import { apiList } from '@/api/wx/jsApiList.js'
import { wxInfo, getUrlParams, stringifyQuery, isWithinRadius } from "@/utils/tools";
import { wxInfo, isWithinRadius } from "@/utils/tools";
import { styleColor } from "@/constant.js";
import { getFormSettingAPI } from "@/api/form.js";
import { showDialog, showConfirmDialog } from 'vant';
// import fp3 from '@/utils/fp3';
import { Updater } from '@/utils/versionUpdater';
import { resetDialogState } from '@/utils/dialogControl.js';
import { useAuthRedirect } from '@/composables';
// 使用 include + pinia 状态管理动态缓存页面
const store = mainStore();
const keepPages = computed(() => store.getKeepPages);
// 表单配置和授权判断完成前,先只显示加载态,避免用户先看到表单又被授权回跳打断
const app_bootstrap_ready = ref(false);
// TAG: 全局设置页面标题
const $route = useRoute();
// watchEffect(() => useTitle("表单标题"));
// 监听路由变化
// 切换路由页面返回顶部
......@@ -56,12 +62,19 @@ const $router = useRouter();
// className: 'zIndex'
// });
// web端判断
const is_pc = computed(() => wxInfo().isPC);
// 微信端判断
const is_wx = computed(() => wxInfo().isWeiXin);
// iframe模式判断
const is_iframe = computed(() => window.self !== window.top);
const {
getQueryValue,
getCurrentTargetHash,
redirectToOpenIdAuth,
savePendingAuthReturnGuard,
consumePendingAuthReturnGuard,
installAuthBackHistoryGuard,
markSkipCycleCheckForAuth,
} = useAuthRedirect();
// 组件卸载时清理定时器(必须在 setup 同步阶段注册钩子)
let upDater = null;
......@@ -72,180 +85,202 @@ onUnmounted(() => {
});
onMounted(async () => {
// 重置弹框状态,确保每次访问页面时都能正常显示未完成表单弹框
resetDialogState();
let leaving_current_page = false;
const code = getUrlParams(location.href) ? getUrlParams(location.href).code : '';
const model = getUrlParams(location.href) ? getUrlParams(location.href).model : '';
const data_id = getUrlParams(location.href) ? getUrlParams(location.href).data_id : '';
// 权限控制, 页面参数判断页面功能
/**
* add 新增页
* info 详情页
* edit 编辑页
* flow 流程页
*/
const page_type = getUrlParams(location.href) ? getUrlParams(location.href).page_type : '';
const raw_url = encodeURIComponent(location.pathname + location.hash);
const flow_node_code = getUrlParams(location.href) ? getUrlParams(location.href).flow_node_code : ''; // flow_node_code 表示随机选择的流程节点的ID
const force_back = getUrlParams(location.href) ? getUrlParams(location.href).force_back : ''; // force_back=1 时,强制按照后台用户模式检查权限
const x_cycle = getUrlParams(location.href) ? getUrlParams(location.href).x_cycle : ''; // 周期ID标识
const volunteer_source = getUrlParams(location.href) ? getUrlParams(location.href).volunteer_source : ''; // 义工来源
// iframe传值openid
const iframe_openid = getUrlParams(location.href) ? getUrlParams(location.href).openid : '';
// 数据收集设置
const { data } = await getFormSettingAPI({ form_code: code, page_type, data_id, flow_node_code, force_back, x_cycle, volunteer_source, openid: iframe_openid });
const form_setting = {};
if (data.length) {
Object.assign(form_setting, data[0]['property_list'], data[0]['extend']);
}
// TAG: 西园寺特有,拿到新域名跳转
if (!import.meta.env.DEV && form_setting.redirect_host) {
let host = location.host;
let url = location.href;
let new_url = url.replace(host, form_setting.redirect_host);
location.href = new_url;
}
// TAG: 是否显示流程按钮
if (page_type === 'add' && form_setting.flow_id) {
form_setting.is_flow = true;
}
if (page_type === 'flow') {
form_setting.is_flow = true;
}
// 缓存表单设置
store.changeFormSetting(form_setting);
// TAG: 跳转未授权页
if (form_setting.auth_error) { // 权限报错信息存在
$router.replace({
path: '/no_auth',
query: {
code,
data_id
}
});
}
// 没有授权判断
let open_auth = form_setting.wxzq_enable && !form_setting.x_field_weixin_openid;
// iframe传值openid
if (iframe_openid) { // 如果获取到iframe传值openid 不再校验授权
open_auth = false;
}
const no_preview_model = model !== 'preview';
try {
// 重置弹框状态,确保每次访问页面时都能正常显示未完成表单弹框
resetDialogState();
let record_openid = false; // 是否记录open_id
/**
* is_back_user 用户登录情况
* 1. 后台用户已登录, 新增表单(无data_id), 为了记录 openid 如果有微信增强设置,则启用微信增强
* 2. 后台用户未登录, 启用微信增强
*/
// if (!form_setting.is_back_user) { // 用户未登录
if (force_back !== '1') { // 非后台用户模式
record_openid = true;
} else { // 用户已登录
if (!is_iframe.value && page_type === 'add') { // 非iframe里面新增表单
record_openid = true;
const code = getQueryValue('code');
const model = getQueryValue('model');
const data_id = getQueryValue('data_id');
// 权限控制, 页面参数判断页面功能
/**
* add 新增页
* info 详情页
* edit 编辑页
* flow 流程页
*/
const page_type = getQueryValue('page_type');
const raw_url = encodeURIComponent(location.pathname + location.hash);
const flow_node_code = getQueryValue('flow_node_code'); // flow_node_code 表示随机选择的流程节点的ID
const force_back = getQueryValue('force_back'); // force_back=1 时,强制按照后台用户模式检查权限
const x_cycle = getQueryValue('x_cycle'); // 周期ID标识
const volunteer_source = getQueryValue('volunteer_source'); // 义工来源
// iframe传值openid
const iframe_openid = getQueryValue('openid');
// 数据收集设置
const { data } = await getFormSettingAPI({ form_code: code, page_type, data_id, flow_node_code, force_back, x_cycle, volunteer_source, openid: iframe_openid });
const form_setting = {};
if (data.length) {
Object.assign(form_setting, data[0]['property_list'], data[0]['extend']);
}
}
// 需要网页授权-必须要域名相同,需要上传到线上测试
/**
* wxzq_scope 微信公众号授权模式
* 空字符串=不授权,snsapi_base=静默授权,snsapi_userinfo=显式授权
*/
// 非测试环境,没有openid信息,需要授权
if (!import.meta.env.DEV && open_auth && form_setting.wxzq_scope && record_openid) {
// 预览模式不开启
if (no_preview_model) {
// 设置标识,让路由守卫跳过首次周期检查
sessionStorage.setItem('skip_cycle_check_for_auth', 'true');
// TAG: 西园寺特有,拿到新域名跳转
if (!import.meta.env.DEV && form_setting.redirect_host) {
let host = location.host;
let url = location.href;
let new_url = url.replace(host, form_setting.redirect_host);
leaving_current_page = true;
location.href = new_url;
return;
}
// TAG: 是否显示流程按钮
if (page_type === 'add' && form_setting.flow_id) {
form_setting.is_flow = true;
}
if (page_type === 'flow') {
form_setting.is_flow = true;
}
// 缓存表单设置
store.changeFormSetting(form_setting);
// TAG: 跳转未授权页
if (form_setting.auth_error) { // 权限报错信息存在
$router.replace({
path: '/auth',
path: '/no_auth',
query: {
href: location.hash,
code
code,
data_id
}
});
return;
}
} else {
// 启用微信增强,非预览模式
if (form_setting.wxzq_enable && no_preview_model) {
const wxJs = await wxJsAPI({ form_code: code, url: raw_url });
wxJs.data.jsApiList = apiList;
wx.config(wxJs.data);
wx.ready(() => {
wx.showAllNonBaseMenuItem();
// TAG:判断定位填表功能, 可能会弹出来上一次的表单提示,因为如果定位正确时还是需要恢复相应的表单
let open_location = form_setting.geofence_enable;
if (force_back !== '1' && open_location) { // 非后台用户模式
const targetLat = form_setting.geofence_center_latitude;
const targetLng = form_setting.geofence_center_longitude;
const radius = form_setting.geofence_circle_radius; // 半径 1000 米
wx.getLocation({
type: 'gcj02', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
success: function (res) {
let currentLat = res.latitude; // 纬度,浮点数,范围为90 ~ -90
let currentLng = res.longitude; // 经度,浮点数,范围为180 ~ -180。
let is_in = isWithinRadius(currentLat, currentLng, targetLat, targetLng, radius);
if (!is_in) {
// 表单定位错误
$router.push("/stop?status=location_error&latitude=" + targetLat + "&longitude=" + targetLng);
}
},
fail: function (res) {
$router.push("/stop?status=get_location_fail");
},
});
}
});
wx.error((err) => {
console.warn(err);
});
// 没有授权判断
let open_auth = form_setting.wxzq_enable && !form_setting.x_field_weixin_openid;
// iframe传值openid
if (iframe_openid) { // 如果获取到iframe传值openid 不再校验授权
open_auth = false;
}
// 判断跳转页面
const no_preview_model = model !== 'preview';
let record_openid = false; // 是否记录open_id
/**
* is_back_user 用户登录情况
* 1. 后台用户已登录, 新增表单(无data_id), 为了记录 openid 如果有微信增强设置,则启用微信增强
* 2. 后台用户未登录, 启用微信增强
*/
// if (!form_setting.is_back_user) { // 用户未登录
if (force_back !== '1') { // 非后台用户模式
// 开启后有开始和结束时间,不在时间范围的显示表单还未开始或者已经结束
if (form_setting.sjsj_is_time_range && form_setting.sjsj_is_time_range) {
// 未开始
if (form_setting.server_time < form_setting.sjsj_begin_time) {
$router.push("/stop?status=apply");
}
// 已结束
if (form_setting.server_time > form_setting.sjsj_end_time) {
$router.push("/stop?status=finish");
record_openid = true;
} else { // 用户已登录
if (!is_iframe.value && page_type === 'add') { // 非iframe里面新增表单
record_openid = true;
}
}
// 需要网页授权-必须要域名相同,需要上传到线上测试
/**
* wxzq_scope 微信公众号授权模式
* 空字符串=不授权,snsapi_base=静默授权,snsapi_userinfo=显式授权
*/
// 非测试环境,没有openid信息,需要授权
if (!import.meta.env.DEV && open_auth && form_setting.wxzq_scope && record_openid) {
// 预览模式不开启
if (no_preview_model) {
const currentTargetHash = getCurrentTargetHash();
// 记录这次授权前的目标页和历史长度,授权成功回跳后要用来修正浏览器后退行为
savePendingAuthReturnGuard(code, currentTargetHash);
// 设置标识,让路由守卫在授权返回后的首轮路由里跳过一次周期检查
markSkipCycleCheckForAuth();
// 直接跳去后端授权地址,不再先经过站内 /auth 中转页,避免浏览器历史里留下 auth 记录
leaving_current_page = true;
redirectToOpenIdAuth(code, currentTargetHash);
return;
}
} else {
if (no_preview_model) {
const currentTargetHash = getCurrentTargetHash();
const authReturnGuard = consumePendingAuthReturnGuard(code, currentTargetHash);
// 只有授权前原本就有上一页时,才需要拦住第一次后退并跨过中间的授权地址
if (authReturnGuard?.historyLength > 1) {
installAuthBackHistoryGuard();
}
}
if (form_setting.sjsj_enable === 0 && !form_setting.sjsj_enable && (page_type === 'add' || page_type === '')) { // 新增的时候才判断
// 表单已结束
$router.push("/stop?status=disable");
// 启用微信增强,非预览模式
if (form_setting.wxzq_enable && no_preview_model) {
const wxJs = await wxJsAPI({ form_code: code, url: raw_url });
wxJs.data.jsApiList = apiList;
wx.config(wxJs.data);
wx.ready(() => {
wx.showAllNonBaseMenuItem();
// TAG:判断定位填表功能, 可能会弹出来上一次的表单提示,因为如果定位正确时还是需要恢复相应的表单
let open_location = form_setting.geofence_enable;
if (force_back !== '1' && open_location) { // 非后台用户模式
const targetLat = form_setting.geofence_center_latitude;
const targetLng = form_setting.geofence_center_longitude;
const radius = form_setting.geofence_circle_radius; // 半径 1000 米
wx.getLocation({
type: 'gcj02', // 默认为wgs84的gps坐标,如果要返回直接给openLocation用的火星坐标,可传入'gcj02'
success: function (res) {
let currentLat = res.latitude; // 纬度,浮点数,范围为90 ~ -90
let currentLng = res.longitude; // 经度,浮点数,范围为180 ~ -180。
let is_in = isWithinRadius(currentLat, currentLng, targetLat, targetLng, radius);
if (!is_in) {
// 表单定位错误
$router.push("/stop?status=location_error&latitude=" + targetLat + "&longitude=" + targetLng);
}
},
fail: function (res) {
$router.push("/stop?status=get_location_fail");
}
});
}
});
wx.error((err) => {
console.warn(err);
});
}
}
// 当数据量达到限额时,该表单将不能继续提交数据。
if (form_setting.sjsj_max_count_error) {
showDialog({
title: '温馨提示',
message: form_setting.sjsj_max_count_error,
theme: 'round-button',
confirmButtonColor: styleColor.baseColor
});
}
// 设定填写次数
if (form_setting.wxzq_scope && no_preview_model) {
if (form_setting.fill_error) {
// 判断跳转页面
// if (!form_setting.is_back_user) { // 用户未登录
if (force_back !== '1') { // 非后台用户模式
// 开启后有开始和结束时间,不在时间范围的显示表单还未开始或者已经结束
if (form_setting.sjsj_is_time_range && form_setting.sjsj_is_time_range) {
// 未开始
if (form_setting.server_time < form_setting.sjsj_begin_time) {
$router.push("/stop?status=apply");
}
// 已结束
if (form_setting.server_time > form_setting.sjsj_end_time) {
$router.push("/stop?status=finish");
}
}
if (form_setting.sjsj_enable === 0 && !form_setting.sjsj_enable && (page_type === 'add' || page_type === '')) { // 新增的时候才判断
// 表单已结束
$router.push("/stop?status=disable");
}
}
// 当数据量达到限额时,该表单将不能继续提交数据。
if (form_setting.sjsj_max_count_error) {
showDialog({
title: '温馨提示',
message: form_setting.fill_error,
message: form_setting.sjsj_max_count_error,
theme: 'round-button',
confirmButtonColor: styleColor.baseColor
});
}
// 设定填写次数
if (form_setting.wxzq_scope && no_preview_model) {
if (form_setting.fill_error) {
showDialog({
title: '温馨提示',
message: form_setting.fill_error,
theme: 'round-button',
confirmButtonColor: styleColor.baseColor
});
}
}
if (is_wx.value) {
document.getElementById('app').style.maxWidth = '100vw';
}
}
if (is_wx.value) {
document.getElementById('app').style.maxWidth = '100vw';
} finally {
// 只有在当前页不需要立刻跳去授权/跳域名时,才让真实页面开始渲染
if (!leaving_current_page && !app_bootstrap_ready.value) {
app_bootstrap_ready.value = true;
}
}
......@@ -301,6 +336,22 @@ body {
position: relative;
}
.app-shell {
min-height: 100vh;
}
.app-bootstrap-loading {
position: fixed;
inset: 0;
z-index: 5000;
display: flex;
align-items: center;
justify-content: center;
padding: 0 24px;
box-sizing: border-box;
background: #ffffff;
}
.@{prefix} {
color: red;
}
......
......@@ -7,6 +7,7 @@
* @Description:
*/
import { onMounted, onUnmounted } from 'vue'
export { useAuthRedirect } from './useAuthRedirect.js'
/**
* 添加和清除 DOM 事件监听器
......
/*
* @Date: 2026-05-29 00:00:00
* @Description: 授权跳转辅助逻辑
*/
import { useRoute } from 'vue-router';
// 授权返回后的首轮路由检查只需要跳过一次,避免刚拿到 openid 又被周期选择打断
const SKIP_CYCLE_CHECK_FOR_AUTH_KEY = 'skip_cycle_check_for_auth';
// 只记录“这一轮刚发起过授权”的短期状态,供授权返回后修正浏览器后退行为
const AUTH_RETURN_GUARD_KEY = 'data-table-auth-return-guard';
// 只保留很短时间,避免授权链路中断后残留旧标记
const AUTH_RETURN_GUARD_TTL = 10 * 60 * 1000;
const toStringQueryValue = (value) => (typeof value === 'string' ? value : '');
const getQueryValueFromString = (search, key) => {
if (!search) return '';
const params = new URLSearchParams(search.startsWith('?') ? search : `?${search}`);
return params.get(key) || '';
};
const safeDecodeURIComponent = (value) => {
try {
return decodeURIComponent(value);
} catch (error) {
return value;
}
};
/**
* 消耗一次“跳过首次周期检查”的标记。
* 命中后会立即清理,确保它只影响授权返回的这一轮路由。
* @returns {boolean} 是否需要跳过一次周期检查
*/
export const consumeSkipCycleCheckForAuth = () => {
const shouldSkip = sessionStorage.getItem(SKIP_CYCLE_CHECK_FOR_AUTH_KEY) === 'true';
if (shouldSkip) {
sessionStorage.removeItem(SKIP_CYCLE_CHECK_FOR_AUTH_KEY);
}
return shouldSkip;
};
const clearAuthReturnGuard = () => {
sessionStorage.removeItem(AUTH_RETURN_GUARD_KEY);
};
const readAuthReturnGuard = () => {
try {
const state = JSON.parse(sessionStorage.getItem(AUTH_RETURN_GUARD_KEY) || '{}');
if (!state?.createdAt || Date.now() - state.createdAt > AUTH_RETURN_GUARD_TTL) {
clearAuthReturnGuard();
return null;
}
return state;
} catch (error) {
clearAuthReturnGuard();
return null;
}
};
/**
* 统一管理授权页与业务页之间共用的跳转能力。
* 这里不保存“授权成功”长期标记,避免后端授权失效后前端还拿旧状态误判。
* @param {import('vue-router').RouteLocationNormalizedLoaded} route 当前路由实例
* @returns {{
* getQueryValue: (key: string) => string,
* getCurrentTargetHash: () => string,
* getAuthTargetHash: () => string,
* buildOpenIdAuthUrl: (code: string, targetHash?: string) => string,
* redirectToOpenIdAuth: (code: string, targetHash?: string) => void,
* savePendingAuthReturnGuard: (code: string, targetHash?: string) => void,
* consumePendingAuthReturnGuard: (code: string, targetHash?: string) => { historyLength: number } | null,
* installAuthBackHistoryGuard: () => void,
* markSkipCycleCheckForAuth: () => void,
* consumeSkipCycleCheckForAuth: () => boolean
* }}
*/
export function useAuthRedirect(route = useRoute()) {
const getQueryValue = (key) => {
const routeValue = toStringQueryValue(route.query[key]);
if (routeValue) return routeValue;
// 兼容两类入口:
// 1. hash 路由标准参数:#/path?code=xxx
// 2. 老入口或外部重定向参数:index.html?code=xxx#/path
const searchValue = getQueryValueFromString(location.search, key);
if (searchValue) return searchValue;
const hash = location.hash || '';
const hashQueryIndex = hash.indexOf('?');
if (hashQueryIndex >= 0) {
const hashSearch = hash.slice(hashQueryIndex + 1);
return getQueryValueFromString(hashSearch, key);
}
return '';
};
// 当前表单页在 hash 模式下的完整路由,授权成功后要回到这里
const getCurrentTargetHash = () => `${location.search}${location.hash || `#${route.fullPath}`}`;
// 优先读 target,新链路;兼容 href,避免老链接直接失效
const getAuthTargetHash = () => {
const rawTarget = getQueryValue('target') || getQueryValue('href');
return rawTarget ? safeDecodeURIComponent(rawTarget) : '';
};
/**
* 统一拼接 openid 授权链接。
* 新链路会在业务页里直接 replace 到这个地址,避免先进入站内 auth 页留下额外历史记录。
* @param {string} code 表单 code
* @param {string} targetHash 授权成功后需要回到的 hash 路由
* @returns {string} 可直接跳转的授权地址
*/
const buildOpenIdAuthUrl = (code, targetHash = getCurrentTargetHash()) => {
const rawUrl = encodeURIComponent(`${location.origin}${location.pathname}${targetHash}`);
const shortUrl = `/srv/?f=custom_form&a=openid&res=${rawUrl}&form_code=${code}`;
// 开发环境继续复用旧的 openid 注入方式,避免影响本地调试体验
if (import.meta.env.DEV) {
return `${shortUrl}&openid=${import.meta.env.VITE_OPENID}`;
}
return shortUrl;
};
/**
* 直接跳到后端 openid 授权地址。
* 使用 location.replace 是为了把“当前未授权页”替换掉,避免浏览器后退时再落回授权中转页。
* @param {string} code 表单 code
* @param {string} targetHash 授权成功后需要回到的 hash 路由
*/
const redirectToOpenIdAuth = (code, targetHash = getCurrentTargetHash()) => {
window.location.replace(buildOpenIdAuthUrl(code, targetHash));
};
/**
* 记录一次待返回的授权流程。
* 这里只保存当前目标页和发起前的历史长度,返回后会立刻消费掉,不作为长期登录态判断。
* @param {string} code 表单 code
* @param {string} targetHash 授权成功后需要回到的 hash 路由
*/
const savePendingAuthReturnGuard = (code, targetHash = getCurrentTargetHash()) => {
sessionStorage.setItem(
AUTH_RETURN_GUARD_KEY,
JSON.stringify({
code,
targetHash,
historyLength: window.history.length,
createdAt: Date.now(),
}),
);
};
/**
* 消费一次授权返回标记。
* 只有当前页面和发起授权时记录的目标页一致,才认为这是同一轮授权返回。
* @param {string} code 表单 code
* @param {string} targetHash 当前页面 hash
* @returns {{ historyLength: number } | null} 命中的返回信息
*/
const consumePendingAuthReturnGuard = (code, targetHash = getCurrentTargetHash()) => {
const state = readAuthReturnGuard();
if (!state) return null;
const matched = state.code === code && state.targetHash === targetHash;
clearAuthReturnGuard();
if (!matched) {
return null;
}
return {
historyLength: Number(state.historyLength) || 0,
};
};
/**
* 在授权成功回到表单后,临时插入一个“同地址跳板”。
* 这样用户第一次点浏览器后退时,会先回到这个跳板,然后我们再一次性跳过授权地址回到真正的上一页。
*/
const installAuthBackHistoryGuard = () => {
const guardKey = `auth-back-guard:${Date.now()}`;
const currentState = window.history.state || {};
// 当前记录标成“跳板底座”
window.history.replaceState(
{
...currentState,
__authBackGuardBase: guardKey,
},
'',
location.href,
);
// 再压入一个相同地址的“顶层占位”,让第一次后退先落到上面的底座状态
window.history.pushState(
{
...currentState,
__authBackGuardTop: guardKey,
},
'',
location.href,
);
const handlePopState = (event) => {
if (event.state?.__authBackGuardBase !== guardKey) {
return;
}
window.removeEventListener('popstate', handlePopState);
// 当前位置已经从顶层占位退回到底座了,再退两层即可跨过授权地址回到真实上一页
window.history.go(-2);
};
window.addEventListener('popstate', handlePopState);
};
/**
* 标记这次授权流程已经进入外部跳转阶段。
* 这个标记只用于让路由守卫在授权返回时跳过一次周期检查。
*/
const markSkipCycleCheckForAuth = () => {
sessionStorage.setItem(SKIP_CYCLE_CHECK_FOR_AUTH_KEY, 'true');
};
return {
getQueryValue,
getCurrentTargetHash,
getAuthTargetHash,
buildOpenIdAuthUrl,
redirectToOpenIdAuth,
savePendingAuthReturnGuard,
consumePendingAuthReturnGuard,
installAuthBackHistoryGuard,
markSkipCycleCheckForAuth,
consumeSkipCycleCheckForAuth,
};
}
......@@ -13,6 +13,7 @@ import { Loading } from "vant";
import Cookies from 'js-cookie';
import { showUnfinishedFormDialog, resetDialogState } from '@/utils/dialogControl.js';
import { getCycleListAPI } from '@/api/cycle';
import { consumeSkipCycleCheckForAuth } from '@/composables/useAuthRedirect.js';
// TAG: 路由配置表
/**
......@@ -145,13 +146,8 @@ router.beforeEach((to, from, next) => {
return;
}
// 检查是否从授权页面返回,如果是首次访问且可能需要授权,则跳过周期检查
const isFromAuth = from.path === '/auth';
const skipCycleCheck = sessionStorage.getItem('skip_cycle_check_for_auth');
// 如果不是从授权页面返回,且设置了跳过标识,则先清除标识并跳过周期检查
if (!isFromAuth && skipCycleCheck === 'true') {
sessionStorage.removeItem('skip_cycle_check_for_auth');
// 授权成功回到业务页后的第一轮路由里,跳过一次周期检查,避免刚拿到 openid 又被打断
if (consumeSkipCycleCheckForAuth()) {
// 直接执行表单检查逻辑,跳过周期检查
if (to.query.page_type === 'add' || to.query.page_type === undefined) {
const existingCookie = Cookies.get(to.query.code);
......
......@@ -11,24 +11,23 @@
<script setup>
import { onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { useAuthRedirect } from '@/composables'
const $route = useRoute();
const {
getAuthTargetHash,
getQueryValue,
redirectToOpenIdAuth,
savePendingAuthReturnGuard,
markSkipCycleCheckForAuth,
} = useAuthRedirect();
onMounted(() => {
// php需要先跳转链接获取openid
/**
* encodeURIComponent() 函数可把字符串作为 URI 组件进行编码。
* 该方法不会对 ASCII 字母和数字进行编码,也不会对这些 ASCII 标点符号进行编码: - _ . ! ~ * ' ( ) 。
* 其他字符(比如 :;/?:@&=+$,# 这些用于分隔 URI 组件的标点符号),都是由一个或多个十六进制的转义序列替换的。
*/
let raw_url = encodeURIComponent(location.origin + location.pathname + $route.query.href); // 未授权的地址
// TAG: 开发环境测试数据
const short_url = `/srv/?f=custom_form&a=openid&res=${raw_url}&form_code=${$route.query.code}`;
// 使用 replace 方法替代 href,避免在浏览器历史中留下记录
// 这样用户点击后退按钮时不会回到授权页面
window.location.replace(import.meta.env.DEV
? `${short_url}&openid=${import.meta.env.VITE_OPENID}`
: `${short_url}`);
const code = getQueryValue('code');
const targetHash = getAuthTargetHash();
// 兼容老的 /auth 入口时,也补上同一套一次性回退保护和周期检查跳过标记
savePendingAuthReturnGuard(code, targetHash || '#/');
markSkipCycleCheckForAuth();
// 兼容老的 /auth 入口:统一交给 composable 计算并 replace 到后端授权地址
redirectToOpenIdAuth(code, targetHash || '#/');
})
</script>
......