feat(@vben/web-antd): 企业微信扫码登录及绑定(适配 hash 路由)
- 登录页新增企业微信扫码登录入口(TDesign 官方图标) - 个人中心开放企业微信绑定/解绑功能 - 适配 hash 路由模式:OAuth 回调 code 在 URL query 中, 通过路由守卫转存 sessionStorage 并重定向到个人中心处理绑定 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,12 +49,40 @@ function setupCommonGuard(router: Router) {
|
||||
* @param router
|
||||
*/
|
||||
function setupAccessGuard(router: Router) {
|
||||
// 一次性检查:hash 路由模式下,OAuth 回调的 code/state 在 URL query(?code=xxx)中,
|
||||
// Vue Router 读不到,需要转存到 sessionStorage 供个人中心绑定页面使用
|
||||
const pendingBind = sessionStorage.getItem('socialBindAction');
|
||||
if (pendingBind === 'bind') {
|
||||
const url = new URL(window.location.href);
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
if (code) {
|
||||
sessionStorage.setItem('socialBindCode', code);
|
||||
sessionStorage.setItem('socialBindState', state || '');
|
||||
sessionStorage.removeItem('socialBindAction');
|
||||
// 清理 URL 中的 OAuth 参数
|
||||
url.searchParams.delete('code');
|
||||
url.searchParams.delete('state');
|
||||
url.searchParams.delete('appid');
|
||||
window.history.replaceState({}, '', url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
router.beforeEach(async (to, from) => {
|
||||
const accessStore = useAccessStore();
|
||||
const userStore = useUserStore();
|
||||
const authStore = useAuthStore();
|
||||
const dictStore = useDictStore();
|
||||
|
||||
// 社交绑定回调:检测到待处理的绑定参数,重定向到个人中心处理
|
||||
if (
|
||||
sessionStorage.getItem('socialBindCode') &&
|
||||
accessStore.accessToken &&
|
||||
to.path !== '/profile'
|
||||
) {
|
||||
return '/profile';
|
||||
}
|
||||
|
||||
// 基本路由,这些路由不需要进入权限拦截
|
||||
if (coreRouteNames.includes(to.name as string)) {
|
||||
if (to.path === LOGIN_PATH && accessStore.accessToken) {
|
||||
|
||||
@@ -241,22 +241,22 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 微信扫码登录 -->
|
||||
<!-- <button
|
||||
class="flex size-12 items-center justify-center rounded-2xl border border-slate-100 bg-slate-50 text-xl text-slate-400 transition-all hover:scale-110 hover:bg-white hover:text-[#46AF35] hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||
title="微信扫码登录"
|
||||
<!-- 企业微信扫码登录 -->
|
||||
<button
|
||||
class="flex size-12 items-center justify-center rounded-2xl border border-slate-100 bg-slate-50 text-xl text-slate-400 transition-all hover:scale-110 hover:bg-white hover:text-[#2BAD13] hover:shadow-md dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
|
||||
title="企业微信扫码登录"
|
||||
type="button"
|
||||
@click="handleThirdLogin(30)"
|
||||
>
|
||||
<svg
|
||||
class="size-6"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M712.149333 352.234667c5.184 0 10.282667 0.064 15.381334 0.341333-26.944-146.837333-178.602667-259.2-361.642667-259.2-202.090667 0-365.888 137.002667-365.888 306.005333 0 99.093333 56.298667 187.178667 143.637333 243.093334l3.349334 2.133333-35.349334 110.72 132.266667-67.370667 6.229333 1.792a431.296 431.296 0 0 0 140.330667 14.848 237.141333 237.141333 0 0 1-11.626667-73.002666c0.021333-154.282667 149.290667-279.36 333.312-279.36z m-218.901333-107.968c28.373333 0 51.349333 22.250667 51.349333 49.728 0 27.456-22.976 49.770667-51.349333 49.770666-28.416 0-51.370667-22.293333-51.370667-49.770666-0.021333-27.498667 22.954667-49.728 51.370667-49.728z m-254.677333 99.477333c-28.394667 0-51.370667-22.293333-51.370667-49.770667 0-27.477333 22.997333-49.728 51.370667-49.728 28.394667 0 51.434667 22.250667 51.434666 49.728s-23.04 49.770667-51.434666 49.770667z" />
|
||||
<path d="M405.76 633.408c0 142.805333 138.453333 258.56 309.162667 258.56a363.392 363.392 0 0 0 103.04-14.762667l111.701333 56.96-29.866667-93.589333 2.816-1.792c73.770667-47.232 121.344-121.621333 121.344-205.397333 0-142.741333-138.389333-258.496-309.056-258.496-170.688 0.042667-309.141333 115.776-309.141333 258.517333z m373.312-89.045333c0-23.168 19.413333-41.962667 43.370667-41.962667 24.021333 0 43.413333 18.816 43.413333 41.962667 0 23.253333-19.413333 42.090667-43.413333 42.090666-23.957333 0-43.370667-18.858667-43.370667-42.090666z m-215.146667 0c0-23.168 19.456-41.962667 43.413334-41.962667 23.978667 0 43.413333 18.816 43.413333 41.962667 0 23.253333-19.434667 42.090667-43.413333 42.090666-23.957333 0-43.413333-18.858667-43.413334-42.090666z" />
|
||||
<svg class="size-6" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="m19.443 17.523l-.075-.015a1.125 1.125 0 0 0-.077-.01a3.912 3.912 0 0 1-2.165-1.196a.304.304 0 0 0-.428 0a.298.298 0 0 0-.021.4l.021.025a.649.649 0 0 0 .042.035l.087.084a3.85 3.85 0 0 1 1.068 1.976a1.38 1.38 0 0 0 .043.255c.056.203.165.395.326.556c.494.49 1.296.49 1.79 0a1.248 1.248 0 0 0-.611-2.11Zm4.187-3.203a1.273 1.273 0 0 0-1.79 0a1.254 1.254 0 0 0-.338.604c-.007.025-.011.05-.016.075c-.004.026-.008.05-.01.076a3.86 3.86 0 0 1-1.078 2.027a3.6 3.6 0 0 1-.127.12a.298.298 0 0 0 0 .425c.11.11.284.116.404.021a.405.405 0 0 0 .058-.063l.085-.086a3.901 3.901 0 0 1 1.993-1.06a1.386 1.386 0 0 0 .257-.043a1.25 1.25 0 0 0 .562-2.096Zm-5.358-3.548a1.248 1.248 0 0 0 0 1.774c.174.173.386.284.61.336l.075.016l.077.01a3.906 3.906 0 0 1 2.165 1.195a.302.302 0 0 0 .427 0a.3.3 0 0 0 .022-.401l-.022-.025a.43.43 0 0 0-.041-.035a3.85 3.85 0 0 1-1.155-2.06a1.38 1.38 0 0 0-.043-.255a1.271 1.271 0 0 0-2.114-.556Zm-1.435 4.633a1.32 1.32 0 0 0 .01-.076a3.86 3.86 0 0 1 1.205-2.148a.298.298 0 0 0 0-.424a.306.306 0 0 0-.428 0c-.014.014-.024.027-.035.041a3.901 3.901 0 0 1-2.077 1.146a1.43 1.43 0 0 0-.257.043a1.248 1.248 0 0 0-.56 2.097c.494.49 1.295.49 1.79 0a1.256 1.256 0 0 0 .352-.68Z"
|
||||
/>
|
||||
<path
|
||||
d="M11.465 3.615a7.865 7.865 0 0 1 7.572 5.738l1.926-.538a9.865 9.865 0 0 0-9.498-7.2c-5.446 0-9.862 4.415-9.862 9.862a9.82 9.82 0 0 0 1.54 5.293l-1.905 4.57h9.022l1.473-.005h.012a9.845 9.845 0 0 0 4.037-.99l-.876-1.797a7.814 7.814 0 0 1-3.205.787l-1.444.005H4.239l1.173-2.815l-.327-.454a7.862 7.862 0 0 1 6.38-12.456Z"
|
||||
/>
|
||||
</svg>
|
||||
</button> -->
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
import type { SystemSocialUserApi } from '#/api/system/social/user';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { confirm } from '@vben/common-ui';
|
||||
import { DICT_TYPE, SystemUserSocialTypeEnum } from '@vben/constants';
|
||||
import { getDictLabel } from '@vben/hooks';
|
||||
import { $t } from '@vben/locales';
|
||||
import { formatDateTime, getUrlValue } from '@vben/utils';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Avatar, Button, Image, message, Tag, Tooltip } from 'ant-design-vue';
|
||||
|
||||
@@ -23,16 +21,11 @@ const emit = defineEmits<{
|
||||
(e: 'update:activeName', v: string): void;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/** 已经绑定的平台 */
|
||||
const bindList = ref<SystemSocialUserApi.SocialUser[]>([]);
|
||||
|
||||
/** 暂不支持钉钉和企业微信,后续开放时移除此过滤 */
|
||||
const HIDDEN_SOCIAL_TYPES = new Set([
|
||||
SystemUserSocialTypeEnum.DINGTALK.type,
|
||||
SystemUserSocialTypeEnum.WECHAT_ENTERPRISE.type,
|
||||
]);
|
||||
/** 暂不支持钉钉,后续开放时移除此过滤 */
|
||||
const HIDDEN_SOCIAL_TYPES = new Set([SystemUserSocialTypeEnum.DINGTALK.type]);
|
||||
|
||||
interface SocialBindItem {
|
||||
title: string;
|
||||
@@ -47,7 +40,7 @@ const allBindList = computed<SocialBindItem[]>(() => {
|
||||
.filter((social) => !HIDDEN_SOCIAL_TYPES.has(social.type))
|
||||
.map((social) => {
|
||||
const socialUser = bindList.value.find(
|
||||
(item) => item.type === social.type,
|
||||
(item) => Number(item.type) === social.type,
|
||||
);
|
||||
return {
|
||||
...social,
|
||||
@@ -80,32 +73,43 @@ async function onBind(bind: SocialBindItem) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const redirectUri = `${location.origin}/profile?${encodeURIComponent(`type=${type}`)}`;
|
||||
// 标记绑定操作,redirect_uri 只用 origin(hash 路由模式下不能带 # 路径)
|
||||
sessionStorage.setItem('socialBindType', String(type));
|
||||
sessionStorage.setItem('socialBindAction', 'bind');
|
||||
const redirectUri = location.origin;
|
||||
window.location.href = await socialAuthRedirect(type, redirectUri);
|
||||
} catch (error) {
|
||||
console.error('社交绑定处理失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 监听路由变化,处理社交绑定回调 */
|
||||
async function bindSocial() {
|
||||
const type = Number(getUrlValue('type'));
|
||||
const code = route.query.code as string;
|
||||
const state = route.query.state as string;
|
||||
if (!code) {
|
||||
/** 处理社交绑定回调(参数由路由守卫从 URL query 转存到 sessionStorage) */
|
||||
async function processPendingBind() {
|
||||
const code = sessionStorage.getItem('socialBindCode');
|
||||
const state = sessionStorage.getItem('socialBindState');
|
||||
const type = Number(sessionStorage.getItem('socialBindType'));
|
||||
if (!code || !type) {
|
||||
return;
|
||||
}
|
||||
await socialBind({ type, code, state });
|
||||
message.success('绑定成功');
|
||||
emit('update:activeName', 'userSocial');
|
||||
await loadBindList();
|
||||
window.history.replaceState({}, '', location.pathname);
|
||||
// 立即清理,防止重复处理
|
||||
sessionStorage.removeItem('socialBindCode');
|
||||
sessionStorage.removeItem('socialBindState');
|
||||
sessionStorage.removeItem('socialBindType');
|
||||
try {
|
||||
await socialBind({ type, code, state: state || '' });
|
||||
message.success('绑定成功');
|
||||
emit('update:activeName', 'userSocial');
|
||||
await loadBindList();
|
||||
} catch (error) {
|
||||
console.error('社交绑定失败:', error);
|
||||
message.error('绑定失败,请重试');
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadBindList();
|
||||
await bindSocial();
|
||||
await processPendingBind();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user