yanghao 10 часов назад
Родитель
Сommit
cb659f9f6e
11 измененных файлов с 307 добавлено и 111 удалено
  1. 4 1
      .env
  2. 2 0
      components.d.ts
  3. 16 0
      src/api/user.ts
  4. BIN
      src/assets/images/code.png
  5. 1 1
      src/main.ts
  6. 0 7
      src/router/index.ts
  7. 5 2
      src/stores/loginStore.ts
  8. 0 1
      src/types/cards.ts
  9. 7 0
      src/types/global.d.ts
  10. 22 58
      src/utils/request.ts
  11. 250 41
      src/views/login.vue

+ 4 - 1
.env

@@ -1 +1,4 @@
-VITE_APP_TITLE=科瑞石油技术门户网站
+VITE_APP_TITLE='科瑞石油技术门户网站'
+
+VITE_DINGTALK_APP_ID='dingmr9ez0ecgbmscfeb'
+VITE_DINGTALK_REDIRECT_URI='http://1.94.244.160:5172/login?loginType=dingding&type=20'

+ 2 - 0
components.d.ts

@@ -12,11 +12,13 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     CardItem: typeof import('./src/components/home/CardItem.vue')['default']
+    CodeLogin: typeof import('./src/components/login/codeLogin.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElCard: typeof import('element-plus/es')['ElCard']
     ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']

+ 16 - 0
src/api/user.ts

@@ -1,5 +1,21 @@
 import http from "@utils/request";
 
+// 钉钉扫码登录
 export const qrcodeLogin = (data: any) => {
   return http.post("/admin-api/system/auth/appSocialLogin", data);
 };
+
+// 刷新令牌
+export const refreshToken = (data: any) => {
+  return http.post("/admin-api/system/auth/refresh-token", data);
+};
+
+// 账号密码登录
+export const login = (data: any) => {
+  return http.post("/admin-api/system/auth/login", data);
+};
+
+// 退出登录
+export const logout = () => {
+  return http.post("/admin-api/system/auth/logout");
+};

BIN
src/assets/images/code.png


+ 1 - 1
src/main.ts

@@ -1,7 +1,7 @@
 import { createApp } from "vue";
 import { createPinia } from "pinia";
 
-import piniaPersist from 'pinia-plugin-persistedstate';
+import piniaPersist from "pinia-plugin-persistedstate";
 
 import "./assets/style/main.css";
 

+ 0 - 7
src/router/index.ts

@@ -9,7 +9,6 @@ import Management from "@/views/management.vue";
 import Command from "@/views/command.vue";
 import ChatBI from "@/views/chatbi.vue";
 import Login from "@/views/login.vue";
-import { useLoginStore } from "@/stores/loginStore";
 
 const routes: RouteRecordRaw[] = [
   {
@@ -50,10 +49,4 @@ const router = createRouter({
 
 const publicPages = ["/", "/login"];
 
-router.beforeEach((to, from, next) => {
-  const loginStore = useLoginStore();
-
-  const authRequired = !publicPages.includes(to.path);
-});
-
 export default router;

+ 5 - 2
src/stores/loginStore.ts

@@ -1,9 +1,12 @@
 import { defineStore } from "pinia";
 import { qrcodeLogin } from "@api/user";
 
-export const useLoginStore = defineStore("login", {
+export const useAuthStore = defineStore("login", {
   state: () => ({
-    userInfo: null,
+    userId: "",
+    accessToken: "",
+    refreshToken: "",
+    expiresTime: "",
   }),
 
   actions: {

+ 0 - 1
src/types/cards.ts

@@ -1 +0,0 @@
-

+ 7 - 0
src/types/global.d.ts

@@ -0,0 +1,7 @@
+export {};
+
+declare global {
+  interface Window {
+    DDLogin: (options: any) => void;
+  }
+}

+ 22 - 58
src/utils/request.ts

@@ -1,5 +1,9 @@
-import axios, { type AxiosResponse } from "axios";
-import { useLoginStore } from "@stores/loginStore";
+import axios, {
+  type AxiosResponse,
+  type InternalAxiosRequestConfig,
+} from "axios";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { useAuthStore } from "@stores/loginStore";
 
 const request = axios.create({
   baseURL: import.meta.env.VITE_BASE_URL as string,
@@ -11,19 +15,22 @@ const request = axios.create({
 
 // 请求拦截:注入 token
 request.interceptors.request.use(
-  (config) => {
-    const loginStore = useLoginStore();
+  (config: InternalAxiosRequestConfig) => {
+    const authStore = useAuthStore();
+
     // 添加 token
-    if (loginStore.token) {
-      config.headers.Authorization = `Bearer ${loginStore.token}`;
+    if (authStore.accessToken) {
+      config.headers.Authorization = `Bearer ${authStore.accessToken}`;
     }
+
     // 添加请求时间戳,防止缓存
-    if (config.method?.toLowerCase() === "get") {
+    if (config.method?.toLowerCase() === "get" && config.params) {
       config.params = {
         ...config.params,
         _t: Date.now(),
       };
     }
+
     return config;
   },
   (error) => {
@@ -32,10 +39,8 @@ request.interceptors.request.use(
   }
 );
 
-// 统一响应处理:提取后端 data 字段并对非成功状态构造错误
 request.interceptors.response.use(
   (response: AxiosResponse) => {
-    // 如果没有业务状态字段,直接返回数据
     return response.data;
   },
   (error) => {
@@ -50,61 +55,20 @@ type ResponseData = {
 };
 
 const http = {
-  get(url: string, data?: any): Promise<ResponseData> {
-    return new Promise((resolve, reject) => {
-      request
-        .get(url, {
-          params: data,
-        })
-        .then((res) => {
-          resolve(res);
-        })
-        .catch((err) => {
-          reject(err);
-        });
-    });
+  async get(url: string, data?: any): Promise<ResponseData> {
+    return await request.get(url, { params: data });
   },
 
-  post(url: string, data?: any): Promise<ResponseData> {
-    return new Promise((resolve, reject) => {
-      request
-        .post(url, data)
-        .then((res) => {
-          resolve(res);
-        })
-        .catch((err) => {
-          reject(err);
-        });
-    });
+  async post(url: string, data?: any): Promise<ResponseData> {
+    return await request.post(url, data);
   },
 
-  put(url: string, data?: any) {
-    return new Promise((resolve, reject) => {
-      request
-        .put(url, data)
-        .then((res) => {
-          resolve(res);
-        })
-        .catch((err) => {
-          reject(err);
-        });
-    });
+  async put(url: string, data?: any): Promise<ResponseData> {
+    return await request.put(url, data);
   },
 
-  delete(url: string, data?: any) {
-    return new Promise((resolve, reject) => {
-      request
-        .delete(url, {
-          params: data,
-        })
-        .then((res) => {
-          resolve(res);
-        })
-        .catch((err) => {
-          reject(err);
-        });
-    });
+  async delete(url: string, data?: any): Promise<ResponseData> {
+    return await request.delete(url, { params: data });
   },
 };
-
 export default http;

+ 250 - 41
src/views/login.vue

@@ -1,6 +1,5 @@
 <template>
   <div class="min-h-screen relative flex items-center justify-center">
-    <!-- Background image -->
     <img
       :src="bgImage"
       alt="background"
@@ -8,60 +7,80 @@
     />
     <div class="absolute inset-0 bg-black/10"></div>
 
-    <!-- Logo and title at top -->
     <div class="absolute top-6 left-6 z-10 flex items-center gap-3">
-      <img :src="logo" alt="logo" class="h-9" />
-      <span class="text-white text-lg font-semibold">智慧经营平台</span>
+      <a href="/" class="flex justify-center gap-2">
+        <img :src="logo" alt="logo" class="h-9" />
+        <span class="text-white text-lg font-semibold mt-1"
+          >DeepOil智慧经营平台</span
+        >
+      </a>
     </div>
 
-    <!-- Login form centered -->
     <div class="relative z-10 w-full max-w-[420px] px-6">
       <div class="bg-white/95 backdrop-blur-sm rounded-lg p-8 shadow-2xl">
         <h1 class="text-2xl font-bold text-center mb-8">登录</h1>
 
-        <div id="login_container" style="width: 200px; height: 200px;"></div>
-        <el-form
-          :model="form"
-          :rules="rules"
-          ref="formRef"
-          label-position="top"
-        >
-          <el-form-item prop="username">
-            <el-input v-model="form.username" placeholder="请输入用户名" />
-          </el-form-item>
-
-          <el-form-item prop="password">
-            <el-input
-              v-model="form.password"
-              type="password"
-              placeholder="请输入密码"
-              show-password
-            />
-          </el-form-item>
-
-          <div class="flex items-center justify-between mb-3">
-            <el-checkbox v-model="form.remember">记住我</el-checkbox>
-            <a class="text-[#409EFF] text-sm cursor-pointer">忘记密码?</a>
+        <!-- 用户名密码登陆 -->
+        <div v-if="!showQrDialog && !showQrOnly">
+          <el-form
+            :model="form"
+            :rules="rules"
+            ref="formRef"
+            label-position="top"
+          >
+            <el-form-item prop="username">
+              <el-input v-model="form.username" placeholder="请输入用户名" />
+            </el-form-item>
+
+            <el-form-item prop="password">
+              <el-input
+                v-model="form.password"
+                type="password"
+                placeholder="请输入密码"
+                show-password
+              />
+            </el-form-item>
+
+            <div class="flex items-center justify-between mb-3">
+              <el-checkbox v-model="form.remember">记住我</el-checkbox>
+              <a class="text-[#409EFF] text-sm cursor-pointer">忘记密码?</a>
+            </div>
+          </el-form>
+
+          <div class="flex flex-col justify-center items-center">
+            <el-button
+              type="primary"
+              class="w-full"
+              :loading="loading"
+              @click="onSubmit"
+              >登录</el-button
+            >
+
+            <div class="w-full mt-3">
+              <el-button class="w-full" @click="qrLogin"
+                >钉钉扫码登录</el-button
+              >
+            </div>
           </div>
+        </div>
 
-          <el-button
-            type="primary"
-            class="w-full"
-            :loading="loading"
-            @click="onSubmit"
-            >登录</el-button
-          >
-          <el-button class="w-full mt-3" @click="qrLogin"
-            >钉钉扫码登录</el-button
-          >
-        </el-form>
+        <!-- 钉钉登陆 -->
+        <div v-show="showQrOnly" class="text-center">
+          <div id="login_container" class="pb-2"></div>
+
+          <div class="flex gap-3 justify-center mt-4">
+            <el-button class="w-full" @click="backToPasswordLogin"
+              >返回用户名密码登录</el-button
+            >
+          </div>
+        </div>
       </div>
     </div>
   </div>
 </template>
 
 <script lang="ts" setup>
-import { reactive, ref } from "vue";
+import { nextTick, onMounted, reactive, ref } from "vue";
 import { ElMessage } from "element-plus";
 import logo from "@/assets/images/logo.png";
 import bgImage from "@/assets/images/bg.png";
@@ -99,7 +118,197 @@ const onSubmit = async () => {
   });
 };
 
+// QR 登录相关状态与方法
+const showQrDialog = ref(false);
+const showQrOnly = ref(false);
+
+const _getRandomString = (len: number) => {
+  len = len || 10;
+  let $chars = "ABCDEFGHIJKMNOPQRSTUVWXYZ";
+  let maxPos = $chars.length;
+  let pwd = "";
+  for (let i = 0; i < len; i++) {
+    pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
+  }
+  return pwd;
+};
+const initDingLogin = () => {
+  let state = _getRandomString(10);
+  const gotoUrl = encodeURIComponent(
+    "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?" +
+      "appid=" +
+      import.meta.env.VITE_DINGTALK_APP_ID +
+      "&response_type=code" +
+      "&scope=snsapi_login" +
+      "&state=" +
+      state +
+      "&redirect_uri=" +
+      import.meta.env.VITE_DINGTALK_REDIRECT_URI
+  );
+
+  nextTick(() => {
+    window.DDLogin({
+      id: "login_container",
+      goto: gotoUrl,
+      style: "border:none;background-color:#FFFFFF;",
+      width: "100%",
+      height: "290",
+    });
+    // 重置扫码登录框的样式,让登录框居中
+    const box = document.getElementById("login_container");
+    if (box) {
+      const iframe = box.querySelector("iframe");
+      if (iframe) {
+        iframe.style.top = "0";
+        iframe.style.bottom = "0";
+        iframe.style.left = "0";
+        iframe.style.right = "0";
+        iframe.style.margin = "auto";
+      }
+    }
+  });
+
+  const handleMessage = function (event: MessageEvent) {
+    if (event.origin === "https://login.dingtalk.com") {
+      let loginTmpCode = event.data;
+      window.location.href =
+        "https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=" +
+        import.meta.env.VITE_DINGTALK_APP_ID +
+        "&response_type=code&scope=snsapi_login&state=" +
+        state +
+        "&redirect_uri=" +
+        import.meta.env.VITE_DINGTALK_REDIRECT_URI +
+        "&loginTmpCode=" +
+        loginTmpCode;
+    }
+  };
+
+  window.addEventListener("message", handleMessage, false);
+};
+
+// const handleDirectLogin = async (loginTmpCode: string): Promise<void> => {
+//   try {
+//     // 调用后端接口,让后端处理钉钉认证
+//     const response = await fetch("/api/auth/dingtalk/login", {
+//       method: "POST",
+//       headers: {
+//         "Content-Type": "application/json",
+//       },
+//       body: JSON.stringify({
+//         loginTmpCode,
+//         state: currentState,
+//       }),
+//     });
+
+//     if (!response.ok) {
+//       throw new Error("登录请求失败");
+//     }
+
+//     const result = await response.json();
+
+//     if (result.success) {
+//       // 登录成功,处理返回的 token 和用户信息
+//       onLoginSuccess(result.data);
+//     } else {
+//       onLoginFailed(result.message || "登录失败");
+//     }
+//   } catch (error) {
+//     console.error("钉钉登录失败:", error);
+
+//   }
+// };
+
 const qrLogin = () => {
-  
+  showQrOnly.value = true;
+  // initDingLogin();
 };
+
+const backToPasswordLogin = () => {
+  showQrOnly.value = false;
+};
+
+onMounted(() => {
+  initDingLogin();
+});
 </script>
+
+<style scoped>
+.qr-dialog {
+  max-height: none;
+}
+
+.qr-dialog .el-dialog__body {
+  overflow: visible;
+  padding: 20px 32px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.qr-dialog .el-dialog__footer {
+  display: flex;
+  justify-content: center;
+  gap: 12px;
+  padding: 12px 24px 20px;
+}
+
+.qr-dialog img {
+  max-width: 100%;
+  height: auto;
+  display: block;
+}
+
+/* 在非常小的视窗内减小二维码尺寸并防止滚动 */
+@media (max-width: 420px) {
+  .qr-dialog .el-dialog {
+    width: calc(100% - 32px) !important;
+  }
+  .qr-dialog img {
+    width: 160px;
+    height: 160px;
+  }
+}
+
+/* 确保扫码登录容器居中 */
+#login_container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+/* 确保 iframe 正确居中 */
+#login_container iframe {
+  position: static !important;
+  margin: 0 auto !important;
+}
+
+/* 增强毛玻璃效果 */
+.bg-white\/95 {
+  background: rgba(255, 255, 255, 0.85) !important;
+  backdrop-filter: blur(10px) !important;
+  -webkit-backdrop-filter: blur(10px) !important;
+  border: 1px solid rgba(255, 255, 255, 0.18);
+  box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
+}
+
+/* 兼容性处理 */
+@supports not (backdrop-filter: blur(10px)) {
+  .bg-white\/95 {
+    background: rgba(255, 255, 255, 0.95) !important;
+  }
+}
+
+/* 确保扫码登录容器居中 */
+#login_container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+/* 确保 iframe 正确居中 */
+#login_container iframe {
+  position: static !important;
+  margin: 0 auto !important;
+}
+</style>