WechatLogin.vue 5.71 KB
<template>
  <div class="wechat-login">
    <el-alert
      v-if="callbackProcessing"
      :title="$t('login.wechatLoginProcessing')"
      type="info"
      show-icon
      :closable="false"
      class="mb-12"
    />

    <div v-if="error" class="wechat-error">{{ error }}</div>

    <div v-if="qr" class="qr-frame">
      <iframe
        :src="qr.qrCodeUrl"
        class="qr-iframe"
        sandbox="allow-scripts allow-same-origin allow-forms allow-top-navigation allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox"
        @load="onIframeLoad"
      ></iframe>
      <div class="qr-actions">
        <el-link type="primary" @click="openInNewTab">{{
          $t("login.wechatLoginOpenInNewTab")
        }}</el-link>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { useRouter, useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import {
  getWechatQrCode,
  checkWechatLoginStatus,
  type WechatQrCodeResponse,
} from "@/api/wechat";
import { useAuthStore } from "@/stores/auth";
import { useI18n } from "vue-i18n";

const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();
const { t } = useI18n();
const loading = ref(false);
const error = ref<string | "">("");
const qr = ref<WechatQrCodeResponse | null>(null);
const callbackProcessing = ref(false);

const onIframeLoad = () => {
  // 仅用于展示加载成功,跨域不可访问内容
};

const openInNewTab = () => {
  if (!qr.value?.qrCodeUrl) return;
  window.open(qr.value.qrCodeUrl, "_blank");
};

const onStartWechatLogin = async () => {
  loading.value = true;
  error.value = "";
  try {
    const mode = (route.query["mode"] as string | undefined) || undefined;
    const { data } = await getWechatQrCode(
      mode === "bind" ? "bind" : undefined,
      window.location.origin,
    );
    qr.value = data;
    // 如果当前是绑定模式,记录 state -> mode 到 sessionStorage,便于回调页识别
    if (mode === "bind" && data?.stateToken) {
      try {
        sessionStorage.setItem(`wechat_mode_${data.stateToken}`, "bind");
      } catch (_) {
        // ignore storage error
      }
    }
  } catch (e: any) {
    error.value =
      e?.response?.data?.message ||
      e?.message ||
      t("login.wechatLoginErrorMessage");
    ElMessage.error(error.value);
  } finally {
    loading.value = false;
  }
};

// 处理回调成功/失败页:/wechat-login?wechat_login=success|error&state=...
const processCallbackIfPresent = async () => {
  const result = route.query["wechat_login"]; // success | error
  const state = route.query["state"] as string | undefined;
  // 优先使用 URL 上的 mode,否则从 sessionStorage 中按 state 读取
  let mode = route.query["mode"] as string | undefined; // bind | undefined
  const message = route.query["message"] as string | undefined;
  if (!result) return;

  callbackProcessing.value = true;
  try {
    if (result === "error") {
      const msg = decodeURIComponent(
        message || t("login.wechatLoginErrorMessage"),
      );
      // 若为未绑定分支(后端回调会带上 mode=needBind),将state作为bindToken传递到登录页
      if (route.query["mode"] === "needBind" && state) {
        try {
          sessionStorage.setItem("wechatBindToken", state);
        } catch (_) {}
        ElMessage.warning(t("login.wechatNeedBind"));
        router.replace({ name: "Auth" });
        return;
      }
      ElMessage.error(msg);
      return;
    }
    if (!state) {
      ElMessage.error(t("login.wechatLoginErrorMessage"));
      return;
    }

    // 绑定模式:绑定已在后端回调完成,这里直接返回设置页
    if (mode === "bind") {
      ElMessage.success(t("login.wechatBindSuccess"));
      router.replace({ path: "/app/settings", query: { tab: "profile" } });
      return;
    }

    const { data } = await checkWechatLoginStatus(state);
    if (data.status === "CONFIRMED" && data.loginResult) {
      const token = data.loginResult.token;
      await authStore.loginWithToken(token);
      ElMessage.success(t("login.wechatLoginSuccess"));
      router.push("/app/welcome");
    } else if (data.status === "EXPIRED") {
      ElMessage.warning(t("login.wechatLoginExpired"));
    } else if (data.status === "CANCELLED") {
      ElMessage.warning(t("login.wechatLoginCancelled"));
    } else {
      ElMessage.warning(data.message || t("login.wechatLoginNotCompleted"));
    }
  } catch (e: any) {
    const msg =
      e?.response?.data?.message ||
      e?.message ||
      t("login.wechatLoginErrorMessage");
    ElMessage.error(msg);
  } finally {
    callbackProcessing.value = false;
    if (state) {
      try {
        sessionStorage.removeItem(`wechat_mode_${state}`);
      } catch (_) {}
    }
  }
};

onMounted(async () => {
  await onStartWechatLogin();
  processCallbackIfPresent();
});
</script>

<style scoped>
.wechat-login {
  margin-top: 16px;
}
.wechat-btn {
  width: 100%;
  height: 44px;
  font-size: 16px;
  font-weight: 500;
  border-radius: 12px;
  border: none;
  background: linear-gradient(135deg, #07c160 0%, #06ae56 100%);
  box-shadow: 0 2px 8px rgba(7, 193, 96, 0.3);
}
.wechat-btn:hover {
  box-shadow: 0 4px 12px rgba(7, 193, 96, 0.4);
}
.wechat-btn:active {
  box-shadow: 0 1px 4px rgba(7, 193, 96, 0.3);
}
.wechat-tip {
  margin-top: 12px;
  color: #606266;
  font-size: 13px;
}
.wechat-error {
  margin-top: 12px;
  color: #f56c6c;
}
.qr-frame {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
}
.qr-iframe {
  width: 100%;
  max-width: 420px;
  height: 520px;
  overflow: hidden;
  background: #fff;
  border: none;
}
.qr-actions {
  margin-top: 8px;
  text-align: center;
}
.mb-12 {
  margin-bottom: 12px;
}
</style>