CzRger hace 2 semanas
padre
commit
8cd2d053d8

+ 9 - 2
.env.development

@@ -2,7 +2,14 @@
 NODE_ENV = development
 
 # 标题
-VITE_TITLE = 学习系统
+VITE_TITLE = 北之星
 
 # 基础路径
-VITE_BASE = 'study-ipad'
+VITE_BASE = 'study-ipad'
+
+# 是否需要登录  Y/N
+VITE_LOGIN_MUST = N
+# token标识
+VITE_TOKEN = siToken
+# 基础代理
+VITE_BASE_API_PROXY = /si-api

+ 8 - 1
.env.production

@@ -5,4 +5,11 @@ NODE_ENV = production
 VITE_TITLE = 学习系统
 
 # 基础路径
-VITE_BASE = 'study-ipad'
+VITE_BASE = 'study-ipad'
+
+# 是否需要登录  Y/N
+VITE_LOGIN_MUST = N
+# token标识
+VITE_TOKEN = siToken
+# 基础代理
+VITE_BASE_API_PROXY = /si-api

BIN
src/assets/images/logo.png


+ 48 - 3
src/router/index.ts

@@ -3,7 +3,7 @@ import Temp404 from '@/views/global/temp/404.vue'
 import RouterView from '@/layout/router-view.vue'
 // @ts-ignore
 import Layout from '@/layout/index.vue'
-import { useMenuStore } from '@/stores'
+import { useAppStore, useMenuStore } from '@/stores'
 
 const routes = [
   { path: '/:pathMatch(.*)*', name: 'NotFound', component: Temp404 },
@@ -15,6 +15,12 @@ const routes = [
     children: [],
   },
   {
+    name: 'login',
+    path: '/login',
+    component: () => import('@/views/global/login/index.vue'),
+    meta: {},
+  },
+  {
     name: 'home',
     path: '/home',
     component: () => import('@/views/study/home/index.vue'),
@@ -125,9 +131,48 @@ const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   routes,
 })
+const hasLogin = (import.meta as any).env.VITE_LOGIN_MUST === 'Y'
 router.beforeEach((to, from, next) => {
-  next()
-  document.getElementById('loader').style.display = 'none'
+  document.title = to.meta?.title || (import.meta as any).env.VITE_TITLE
+  const isLogin = to.name === 'login'
+  const inviteQuery = to.name === 'invite' ? to.query : {}
+  const AppStore = useAppStore()
+  AppStore.loadingStart()
+  if (
+    isLogin &&
+    !AppStore.userInfo &&
+    !localStorage.getItem((import.meta as any).env.VITE_TOKEN)
+  ) {
+    next()
+    AppStore.loadingEnd()
+  } else {
+    if (hasLogin) {
+      if (AppStore.userInfo) {
+        isLogin ? next({ name: 'root' }) : next()
+        AppStore.loadingEnd()
+      } else {
+        if (localStorage.getItem((import.meta as any).env.VITE_TOKEN)) {
+          AppStore.initUserInfo()
+            .then(() => {
+              isLogin ? next({ name: 'root' }) : next()
+            })
+            .catch((e) => {
+              localStorage.removeItem((import.meta as any).env.VITE_TOKEN)
+              next({ name: 'login', query: inviteQuery })
+            })
+            .finally(() => {
+              AppStore.loadingEnd()
+            })
+        } else {
+          next({ name: 'login', query: inviteQuery })
+          AppStore.loadingEnd()
+        }
+      }
+    } else {
+      next()
+      AppStore.loadingEnd()
+    }
+  }
 })
 export const initRoutes = () => {
   const MenuStore = useMenuStore()

+ 1 - 0
src/stores/index.ts

@@ -1,2 +1,3 @@
+export * from './modules/app'
 export * from './modules/dialog'
 export * from './modules/menu'

+ 40 - 0
src/stores/modules/app.ts

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+// import { userInfo } from '@/api/modules/global/login'
+
+export const useAppStore = defineStore('app', {
+  state: () => ({
+    userInfo: null,
+  }),
+  getters: {},
+  actions: {
+    initUserInfo() {
+      return new Promise((resolve, reject) => {
+        setTimeout(() => {
+          resolve(this.userInfo)
+        }, 500)
+        // userInfo()
+        //   .then(({ data }: any) => {
+        //     this.userInfo = data
+        //     this.tenantInfo = data?.tenant
+        //     this.tenants = data?.tenants || []
+        //     resolve(this.userInfo)
+        //   })
+        //   .catch((e) => {
+        //     reject(e)
+        //   })
+      })
+    },
+    loadingStart() {
+      const l = document.getElementById('loader')
+      if (l) {
+        l.style.display = 'flex'
+      }
+    },
+    loadingEnd() {
+      const l = document.getElementById('loader')
+      if (l) {
+        l.style.display = 'none'
+      }
+    },
+  },
+})

+ 192 - 0
src/views/global/login/index.vue

@@ -0,0 +1,192 @@
+<template>
+  <div class="flex size-full items-center justify-center bg-[#f8fafc]">
+    <div
+      class="w-full max-w-[420px] rounded-xl bg-white p-8"
+      style="box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1)"
+    >
+      <!-- Logo -->
+      <div class="mb-8 flex justify-center">
+        <img
+          src="@/assets/images/logo.png"
+          alt="北之星"
+          class="h-20 object-contain"
+        />
+      </div>
+
+      <!-- 登录表单 -->
+      <CzrForm ref="ref_form">
+        <!-- 账号输入框 -->
+        <div class="mb-6 w-full">
+          <label class="mb-1 block text-sm font-medium text-gray-700"
+            >账号</label
+          >
+          <div
+            class="relative block w-full rounded-md border-gray-300 py-3 pl-4 pl-8 shadow-sm focus:outline-none"
+          >
+            <div
+              class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4"
+            >
+              <i class="fas fa-user text-gray-400"></i>
+            </div>
+            <CzrFormColumn
+              required
+              :span="24"
+              label-width="0px"
+              v-model:param="state.form.username"
+              :transparent="true"
+              default-error-msg="请输入账号"
+            />
+          </div>
+        </div>
+        <!-- 密码输入框 -->
+        <div class="mb-6 w-full">
+          <label class="mb-1 block text-sm font-medium text-gray-700"
+            >密码</label
+          >
+          <div
+            class="relative block w-full rounded-md border-gray-300 py-3 pl-4 pl-8 shadow-sm focus:outline-none"
+          >
+            <div
+              class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4"
+            >
+              <i class="fas fa-lock text-gray-400"></i>
+            </div>
+            <CzrFormColumn
+              required
+              :span="24"
+              label-width="0px"
+              v-model:param="state.form.password"
+              type="password"
+              show-password
+              :transparent="true"
+              default-error-msg="请输入密码"
+            />
+          </div>
+        </div>
+
+        <!-- 验证码输入框 -->
+        <div class="mb-6 w-full">
+          <label class="mb-1 block text-sm font-medium text-gray-700"
+            >验证码</label
+          >
+          <div class="grid grid-cols-4 gap-4">
+            <div
+              class="relative col-span-3 block rounded-md border-gray-300 py-3 pl-4 pl-8 shadow-sm focus:outline-none"
+            >
+              <div
+                class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4"
+              >
+                <i class="fas fa-lock text-gray-400"></i>
+              </div>
+              <CzrFormColumn
+                required
+                :span="24"
+                label-width="0px"
+                v-model:param="state.form.code"
+                :transparent="true"
+                default-error-msg="请输入验证码"
+              />
+            </div>
+            <div class="col-span-1 flex items-center">
+              <img
+                src="@/assets/images/logo.png"
+                alt="验证码"
+                class="captcha-img h-10 rounded border border-gray-300"
+              />
+            </div>
+          </div>
+        </div>
+
+        <!-- 登录按钮 -->
+        <div
+          v-loading="state.loading"
+          class="btn-login flex w-full justify-center rounded-md border border-transparent bg-blue-600 px-4 py-3 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none"
+          @click="onLogin"
+        >
+          登录
+        </div>
+      </CzrForm>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { Notify } from 'quasar'
+
+const router = useRouter()
+const state: any = reactive({
+  form: {
+    username: '',
+    password: '',
+    code: '',
+  },
+  loading: false,
+})
+const ref_form = ref()
+const onLogin = () => {
+  if (!state.loading) {
+    ref_form.value
+      .submit()
+      .then(() => {
+        state.loading = true
+        setTimeout(() => {
+          Notify.create({
+            message: '登录成功!',
+            position: 'top',
+            type: 'success',
+          })
+          localStorage.setItem((import.meta as any).env.VITE_TOKEN, 'SSS')
+          router.replace({ name: 'root' })
+          state.loading = false
+        }, 1000)
+        // loginSubmit(state.form)
+        //   .then(({ data }: any) => {
+        //     Notify.create({
+        //       message: '登录成功!',
+        //       position: 'top',
+        //       type: 'success',
+        //     })
+        //     localStorage.setItem((import.meta as any).env.VITE_TOKEN, data)
+        //     router.replace({ name: 'root' })
+        //     state.loading = false
+        //   })
+        //   .catch(() => {})
+        //   .finally(() => {
+        //     state.loading = false
+        //   })
+      })
+      .catch((e) => {
+        Notify.create({
+          message: e[0].message,
+          position: 'top',
+          type: 'warning',
+        })
+      })
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.btn-login {
+  transition: all 0.3s ease;
+}
+.btn-login:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+}
+.captcha-img {
+  cursor: pointer;
+  transition: all 0.3s ease;
+}
+.captcha-img:hover {
+  opacity: 0.8;
+}
+:deep(.el-form-item) {
+  margin-bottom: 0;
+  .el-form-item__error {
+    display: none;
+  }
+}
+</style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 108 - 0
src/views/global/login/北之星登录.html