前端vue2+飞书应用免登录(项目记录)

11 阅读4分钟

功能介绍:飞书应用免登录h5系统。 需求:飞书内打开系统不需要登录,外部浏览器打开系统弹出飞书授权登录

第一步: 首先在index.html中引入sdk

<script src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.26.js"></script>

第二步: 新增一个路由 名字可以自定义 我写的是 loginFeishu

实现思路:1、先判断当前浏览器环境(飞书内or普通浏览器)
2、向后台获取应用id(getAppIdFromServerFun)
3、拿到id后通过h5sdk、tt.requestAccess方法把id给飞书
4、成功回调会给你返回一个code值,这个值给后台,后端给你返回token就就就OVER了。

注:这里面有几个坑 反正奇奇妙妙的
1、redirectUrl似乎必须传 还要通过encodeURIComponent转码
2、redirectUrl一定要在飞书开发者后台应用中配置重定向路径且要和代码写的一致。

  <div class="feishu-auth-container">
    <!-- 1. 全局加载中状态 -->
    <div class="loading-box" v-if="loading">
      <div class="loading-spinner"></div>
      <span class="loading-text">{{ loadingText }}</span>
    </div>

    <!-- 2. 【新增】登录成功过渡页 -->
    <!-- 当 isSuccess 为 true 时,显示这个,挡住下面的错误页 -->
    <div class="success-box" v-else-if="isSuccess">
      <div class="success-icon">✓</div>
      <div class="success-title">授权成功</div>
      <div class="success-desc">正在为您跳转...</div>
    </div>

    <!-- 3. 只有授权失败,才会走到这里 -->
    <div class="error-box" v-else>
      <div class="error-icon">!</div>
      <div class="error-title">授权失败</div>
      <div class="error-desc">{{ errorMsg }}</div>
      <button class="retry-btn" @click="handleRetry">重新授权</button>
    </div>
  </div>
</template>

<script>
  import { getAppIdFromServerFun, callbackFun } from '@/api/loginFeishu';

  export default {
    name: 'FeishuAuth',
    data() {
      return {
        loading: true,
        loadingText: '正在检测环境...',
        errorMsg: '',
        h5sdk: window.h5sdk,
        tt: window.tt,
        appId: '',
        redirectUrl: redirectUrl: window.location.origin + window.location.pathname,,

        // ★★ 修改1:新增成功状态,默认为 false ★★
        isSuccess: false,
      };
    },
    computed: {
      encodeRedirectUrl() {
        return encodeURIComponent(this.redirectUrl);
      },
    },
    async mounted() {
      const callbackCode = this.getCodeFromUrl();
      if (callbackCode) {
        this.loadingText = '外部授权成功,正在验证信息...';
        await this.getUserInfoByCode(callbackCode);
        return;
      }
      await this.initFeishuAuth();
    },
    methods: {
      getCodeFromUrl() {
        const urlParams = new URLSearchParams(window.location.search);
        return urlParams.get('code') || '';
      },

      async initFeishuAuth() {
        try {
          this.loadingText = '正在获取配置...';
          this.appId = await this.getAppIdFromServer();

          const isInFeishu = this.checkFeishuEnv();

          if (isInFeishu) {
            this.loadingText = '飞书客户端内授权中...';
            await this.getAuthCodeBySdk();
          } else {
            this.loadingText = '正在跳转至飞书授权页面...';
            this.redirectToFeishuAuth();
          }
        } catch (err) {
          this.loading = false;
          this.errorMsg = err.message || '飞书授权失败,请重试';
          // console.error('飞书授权异常:', err);
        }
      },

      checkFeishuEnv() {
        return !!this.h5sdk && !!this.tt;
      },

      async getAppIdFromServer() {
        const res = await getAppIdFromServerFun();
        if (!res.data) {
          throw new Error('获取应用ID失败');
        }
        return res.data;
      },

      getAuthCodeBySdk() {
        return new Promise((resolve, reject) => {
          this.h5sdk.error(err => {
            reject(new Error(`SDK初始化异常: ${JSON.stringify(err)}`));
          });

          this.h5sdk.ready(() => {
            this.tt.requestAccess({
              appID: this.appId,
              scopeList: [],
              redirect_uri: this.encodeRedirectUrl,
              success: res => {
                // console.log('SDK授权成功', res.code);
                this.getUserInfoByCode(res.code);
                resolve();
              },
              fail: err => {
                reject(new Error(`获取免登码失败: ${JSON.stringify(err)}`));
              },
            });
          });
        });
      },

      redirectToFeishuAuth() {
        // console.log('外部浏览器,跳转中...');
        const feishuAuthUrl = `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${this.appId}&response_type=code&redirect_uri=${this.encodeRedirectUrl}&scope=&state=RANDOMSTRING`;
        window.location.href = feishuAuthUrl;
      },

      async getUserInfoByCode(code) {
        try {
          if (!code) throw new Error('授权码为空,验证失败');
          const res = await callbackFun({ code: code });
          // console.log(res);
          if (res.code == 'SUCCESS') {
            this.loginSuccrssFun(res);
          } else {
            this.loading = false;
            this.errorMsg = res.title || '登录验证失败,请重试';
          }
        } catch (err) {
          this.loading = false;
          this.errorMsg = err.title || '网络异常或授权码失效,请重新授权';
          // console.error('授权码验证失败:', err);
        }
      },

      loginSuccrssFun(res) {
        // ★★ 修改2:登录成功逻辑开始时,立即设置 isSuccess = true ★★
        // 这样 v-else-if 会生效,页面显示“授权成功”,而不是“失败”
        this.isSuccess = true;

        // ★★ 修改3:这里不需要等待 setToken 的 Promise 完成再关闭 loading ★★
        // 因为 isSuccess 已经接管了视图,loading 可以直接关,或者留着也没事
        this.loading = false;

        sessionStorage.setItem('id_token', res.data.id_token);
        sessionStorage.setItem('username', res.data.name);
        sessionStorage.setItem('userImgUrl', res.data.imgUrl);
        sessionStorage.setItem('AdminFlag', res.data.flag);

        if (res.data.userLang == 'zh_CN' || res.data.userLang == '') {
          this.$store.commit('setLang', 'cn');
          this.$i18n.locale = 'cn';
        } else {
          this.$store.commit('setLang', 'en');
          this.$i18n.locale = 'en';
        }

        this.$store.commit('setShowFeedBtn', '1');

        // 执行路由跳转
        this.$store.dispatch('setToken', res.data.id_token).then(() => {
          this.$router.replace({ path: '/amkLargeScreen' });
        });
      },

      handleRetry() {
        this.loading = true;
        this.errorMsg = '';
        this.isSuccess = false; // ★★ 修改4:重试时重置成功状态 ★★
        window.history.replaceState(
          {},
          document.title,
          window.location.pathname
        );
        this.initFeishuAuth();
      },
    },
  };
</script>

<style scoped>
  .feishu-auth-container {
    width: 100vw;
    height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background-color: #f7f8fa;
    font-family: -apple-system, sans-serif;
  }

  /* 加载动画优化 */
  .loading-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 16px;
  }

  .loading-spinner {
    width: 40px;
    height: 40px;
    border: 4px solid rgba(0, 125, 255, 0.1);
    border-left-color: #007dff;
    border-radius: 50%;
    animation: spin 1s linear infinite;
  }

  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }

  .loading-text {
    font-size: 15px;
    color: #667085;
  }

  /* 错误页面样式 */
  .error-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 0 20px;
  }

  .error-icon {
    width: 120px;
    height: 120px;
    line-height: 120px;
    background: #fff2e8;
    color: #ff7d00;
    font-size: 60px;
    font-weight: bold;
    border-radius: 50%;
    margin-bottom: 12px;
  }

  .error-title {
    font-size: 22px;
    color: #f53f3f;
    font-weight: 500;
    margin-bottom: 8px;
  }

  .error-desc {
    font-size: 20px;
    color: #667085;
    line-height: 1.5;
    margin-bottom: 20px;
  }

  .retry-btn {
    padding: 8px 24px;
    background-color: #007dff;
    color: #fff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    transition: background 0.2s;
  }

  .retry-btn:hover {
    background-color: #0066cc;
  }

  /* ★★ 新增样式:成功页面 ★★ */
  .success-box {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 0 20px;
  }

  .success-icon {
    width: 120px;
    height: 120px;
    line-height: 120px;
    background: #e8ffec; /* 淡绿色背景 */
    color: #00b42a; /* 绿色对勾 */
    font-size: 60px;
    font-weight: bold;
    border-radius: 50%;
    margin-bottom: 12px;
  }

  .success-title {
    font-size: 22px;
    color: #00b42a; /* 绿色标题 */
    font-weight: 500;
    margin-bottom: 8px;
  }

  .success-desc {
    font-size: 20px;
    color: #667085;
    line-height: 1.5;
  }
</style>