feat(ui): 新增登录
							
								
								
									
										8
									
								
								gpt-vue/projects/vue-admin/env.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1 +1,9 @@
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
declare module "*.vue" {
 | 
			
		||||
  import { DefineComponent } from "vue";
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
 | 
			
		||||
  const component: DefineComponent<{}, {}, any>;
 | 
			
		||||
  export default component;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare const __AUTH_KEY: string;
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="/favicon.ico">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Vite App</title>
 | 
			
		||||
    <title>ChatPlus-Ai</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 66 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/alipay.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/chat.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/mic.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 38 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/mj.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/reward.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 44 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/sd.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/user-info.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/vip.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/wechat-pay.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 48 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/wx.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 36 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/login-content.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 63 KiB  | 
@@ -1,3 +0,0 @@
 | 
			
		||||
VITE_PROXY_BASE_URL="/api"
 | 
			
		||||
VITE_TARGET_URL="http://172.22.11.2:5678"
 | 
			
		||||
VITE_SOCKET_IO_URL="http://172.28.1.3:8899"
 | 
			
		||||
@@ -1,3 +0,0 @@
 | 
			
		||||
VITE_PROXY_BASE_URL=""
 | 
			
		||||
VITE_TARGET_URL="/"
 | 
			
		||||
VITE_SOCKET_IO_URL="/"
 | 
			
		||||
@@ -1,3 +1,31 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <RouterView />
 | 
			
		||||
</template>
 | 
			
		||||
<style>
 | 
			
		||||
::-webkit-scrollbar {
 | 
			
		||||
  width: 8px;
 | 
			
		||||
  height: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-thumb {
 | 
			
		||||
  border-radius: 1px;
 | 
			
		||||
  box-shadow: inset 0 0 5px #0000000d;
 | 
			
		||||
  background: #d9d9d9;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::-webkit-scrollbar-track {
 | 
			
		||||
  box-shadow: inset 0 0 5px #0000000d;
 | 
			
		||||
  border-radius: 1px;
 | 
			
		||||
  background: #fafafa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.public-bg {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  width: 100vw;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  background: linear-gradient(133deg, #ffffff 0%, #dde8fe 100%);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,21 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { IconDown, IconExport } from "@arco-design/web-vue/es/icon";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import Logo from "/images/logo.png";
 | 
			
		||||
 | 
			
		||||
import SystemMenu from "./SystemMenu.vue";
 | 
			
		||||
import PageWrapper from "./PageWrapper.vue";
 | 
			
		||||
 | 
			
		||||
const logoWidth = "200px";
 | 
			
		||||
const authStore = useAuthStore();
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <ALayout class="custom-layout">
 | 
			
		||||
    <ALayoutHeader class="custom-layout-header">
 | 
			
		||||
      <div class="logo"></div>
 | 
			
		||||
      <div class="logo">
 | 
			
		||||
        <img :src="Logo" alt="logo" />
 | 
			
		||||
        <span>ChatPlus 控制台</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="action">
 | 
			
		||||
        <ADropdown>
 | 
			
		||||
          <ASpace align="center" :size="4">
 | 
			
		||||
@@ -20,7 +26,11 @@ const logoWidth = "200px";
 | 
			
		||||
            <ADoption value="changeOwnPwd">更改密码</ADoption>
 | 
			
		||||
          </template>
 | 
			
		||||
          <template #footer>
 | 
			
		||||
            <APopconfirm content="确认退出?" position="br">
 | 
			
		||||
            <APopconfirm
 | 
			
		||||
              content="确认退出?"
 | 
			
		||||
              position="br"
 | 
			
		||||
              @ok="authStore.logout"
 | 
			
		||||
            >
 | 
			
		||||
              <ASpace align="center" class="logout-area">
 | 
			
		||||
                <IconExport size="16" />
 | 
			
		||||
                <span>退出</span>
 | 
			
		||||
@@ -52,10 +62,15 @@ const logoWidth = "200px";
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    border-bottom: 1px solid var(--color-neutral-2);
 | 
			
		||||
    .logo {
 | 
			
		||||
      display: block;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      width: v-bind("logoWidth");
 | 
			
		||||
      text-align: center;
 | 
			
		||||
      cursor: pointer;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      gap: 12px;
 | 
			
		||||
      img {
 | 
			
		||||
        width: 30px;
 | 
			
		||||
        height: 30px;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    .action {
 | 
			
		||||
      display: flex;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								gpt-vue/projects/vue-admin/src/composables/useRequest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import type { Ref } from "vue";
 | 
			
		||||
import type { BaseResponse } from "@gpt-vue/packages/type";
 | 
			
		||||
 | 
			
		||||
type Request<T> = (params?: any) => Promise<BaseResponse<T>>
 | 
			
		||||
function useRequest<T>(request: Request<T>) {
 | 
			
		||||
  const result = ref<T>()
 | 
			
		||||
  const loading = ref(false)
 | 
			
		||||
 | 
			
		||||
  const requestData = async (params?: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await request(params)
 | 
			
		||||
      result.value = res.data
 | 
			
		||||
      return Promise.resolve(res)
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      return Promise.reject(err)
 | 
			
		||||
    } finally {
 | 
			
		||||
      loading.value = false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return [requestData, result, loading] as [Request<T>, Ref<T>, Ref<boolean>]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default useRequest
 | 
			
		||||
@@ -7,6 +7,8 @@ export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/common/upload/m
 | 
			
		||||
export const instance = createInstance()
 | 
			
		||||
 | 
			
		||||
instance.interceptors.request.use((config) => {
 | 
			
		||||
  config.headers[__AUTH_KEY] = localStorage.getItem(__AUTH_KEY);
 | 
			
		||||
  config.headers["Authorization"] = localStorage.getItem(__AUTH_KEY);
 | 
			
		||||
  return config;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								gpt-vue/projects/vue-admin/src/http/login.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
			
		||||
import http from "@/http/config";
 | 
			
		||||
 | 
			
		||||
export const userLogin = (data: {
 | 
			
		||||
  username: string;
 | 
			
		||||
  password: string;
 | 
			
		||||
}) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/login",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const userLogout = () => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/logout",
 | 
			
		||||
    method: "get",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getSession = () => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/session",
 | 
			
		||||
    method: "get",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -1,7 +1,21 @@
 | 
			
		||||
import { createRouter, createWebHistory } from 'vue-router'
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import CustomLayout from '@/components/CustomLayout.vue'
 | 
			
		||||
import menu from './menu'
 | 
			
		||||
 | 
			
		||||
const whiteListRoutes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: "/login",
 | 
			
		||||
    name: "Login",
 | 
			
		||||
    component: () => import("@/views/LoginView.vue"),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: "/:pathMatch(.*)*",
 | 
			
		||||
    name: "404",
 | 
			
		||||
    component: () => import("@/views/NotFound.vue"),
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(import.meta.env.BASE_URL),
 | 
			
		||||
  routes: [
 | 
			
		||||
@@ -12,12 +26,28 @@ const router = createRouter({
 | 
			
		||||
      redirect: () => menu[0].path,
 | 
			
		||||
      children: menu
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: "/:pathMatch(.*)*",
 | 
			
		||||
      name: "404",
 | 
			
		||||
      component: () => import("@/views/NotFound.vue"),
 | 
			
		||||
    },
 | 
			
		||||
    ...whiteListRoutes
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const whiteList = whiteListRoutes.map((i) => i.name);
 | 
			
		||||
 | 
			
		||||
router.beforeEach((to, _, next) => {
 | 
			
		||||
  const authStore = useAuthStore();
 | 
			
		||||
  authStore.init()
 | 
			
		||||
  if (typeof to.name === "string" && whiteList.includes(to.name)) {
 | 
			
		||||
    if (authStore.token && to.name === "Login") {
 | 
			
		||||
      next({ path: menu[0].path });
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    next();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!authStore.token) {
 | 
			
		||||
    next({ name: "Login" });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default router
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								gpt-vue/projects/vue-admin/src/stores/auth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
import { Message } from '@arco-design/web-vue'
 | 
			
		||||
import { userLogin, userLogout } from '@/http/login'
 | 
			
		||||
import router from '@/router'
 | 
			
		||||
 | 
			
		||||
export const useAuthStore = defineStore({
 | 
			
		||||
  id: __AUTH_KEY,
 | 
			
		||||
  state: () => ({ token: null }),
 | 
			
		||||
  actions: {
 | 
			
		||||
    init() {
 | 
			
		||||
      this.$state.token = localStorage.getItem(__AUTH_KEY);
 | 
			
		||||
    },
 | 
			
		||||
    async login(params) {
 | 
			
		||||
      try {
 | 
			
		||||
        const { data } = await userLogin(params)
 | 
			
		||||
        if (data) {
 | 
			
		||||
          this.$state.token = data;
 | 
			
		||||
          localStorage.setItem(__AUTH_KEY, data)
 | 
			
		||||
          Message.success('登录成功');
 | 
			
		||||
          router.replace({ name: 'home' })
 | 
			
		||||
          return Promise.resolve(data)
 | 
			
		||||
        }
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        return Promise.reject(err)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    async logout() {
 | 
			
		||||
      try {
 | 
			
		||||
        await userLogout()
 | 
			
		||||
        if (this.$state.token) {
 | 
			
		||||
          localStorage.removeItem(__AUTH_KEY)
 | 
			
		||||
          this.$restate.token = null
 | 
			
		||||
        }
 | 
			
		||||
        Message.success('退出成功');
 | 
			
		||||
        router.push({ name: 'Login' })
 | 
			
		||||
        return Promise.resolve(true)
 | 
			
		||||
      } catch (err) {
 | 
			
		||||
        return Promise.reject(err)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										92
									
								
								gpt-vue/projects/vue-admin/src/views/LoginView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,92 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { reactive } from "vue";
 | 
			
		||||
import { useRoute } from "vue-router";
 | 
			
		||||
import { IconUser, IconLock } from "@arco-design/web-vue/es/icon";
 | 
			
		||||
import { useAuthStore } from "@/stores/auth";
 | 
			
		||||
import useRequest from "@/composables/useRequest";
 | 
			
		||||
 | 
			
		||||
const route = useRoute();
 | 
			
		||||
const authStore = useAuthStore();
 | 
			
		||||
 | 
			
		||||
const [loginRequest, _, loading] = useRequest(authStore.login);
 | 
			
		||||
const formData = reactive({
 | 
			
		||||
  username: "",
 | 
			
		||||
  password: "",
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
async function handleSubmit({ errors, values }: any) {
 | 
			
		||||
  if (errors) return;
 | 
			
		||||
  await loginRequest({
 | 
			
		||||
    ...values,
 | 
			
		||||
    ...route.query,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="login-wrapper public-bg">
 | 
			
		||||
    <div class="login-container">
 | 
			
		||||
      <div class="login-box">
 | 
			
		||||
        <AForm :model="formData" size="large" @submit="handleSubmit">
 | 
			
		||||
          <h1 class="title">ChatGPT Plus Admin</h1>
 | 
			
		||||
          <AFormItem
 | 
			
		||||
            hide-label
 | 
			
		||||
            field="username"
 | 
			
		||||
            :rules="[{ required: true, message: '请输入用户名' }]"
 | 
			
		||||
          >
 | 
			
		||||
            <AInput v-model="formData.username" placeholder="用户名">
 | 
			
		||||
              <template #prefix>
 | 
			
		||||
                <IconUser />
 | 
			
		||||
              </template>
 | 
			
		||||
            </AInput>
 | 
			
		||||
          </AFormItem>
 | 
			
		||||
          <AFormItem
 | 
			
		||||
            hide-label
 | 
			
		||||
            field="password"
 | 
			
		||||
            :rules="[{ required: true, message: '请输入密码' }]"
 | 
			
		||||
          >
 | 
			
		||||
            <AInputPassword v-model="formData.password" placeholder="密码">
 | 
			
		||||
              <template #prefix>
 | 
			
		||||
                <IconLock />
 | 
			
		||||
              </template>
 | 
			
		||||
            </AInputPassword>
 | 
			
		||||
          </AFormItem>
 | 
			
		||||
          <AFormItem hide-label>
 | 
			
		||||
            <AButton type="primary" long html-type="submit" :loading="loading">
 | 
			
		||||
              登录
 | 
			
		||||
            </AButton>
 | 
			
		||||
          </AFormItem>
 | 
			
		||||
        </AForm>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<style lang="less" scoped>
 | 
			
		||||
.login-wrapper {
 | 
			
		||||
  .login-container {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    width: 1000px;
 | 
			
		||||
    height: 500px;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: right;
 | 
			
		||||
    background-color: #ffffff;
 | 
			
		||||
    background-image: url("/login-content.png");
 | 
			
		||||
    background-size: contain;
 | 
			
		||||
    background-repeat: no-repeat;
 | 
			
		||||
    background-position: left;
 | 
			
		||||
    border-radius: 40px;
 | 
			
		||||
    .login-box {
 | 
			
		||||
      display: flex;
 | 
			
		||||
      width: 50%;
 | 
			
		||||
      height: 100%;
 | 
			
		||||
      padding: 20px;
 | 
			
		||||
      border-radius: 40px;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
      justify-content: center;
 | 
			
		||||
      box-sizing: border-box;
 | 
			
		||||
      .title {
 | 
			
		||||
        text-align: center;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import http from "@/http/config";
 | 
			
		||||
 | 
			
		||||
export const getList = (params?: Record<string, unknown>) => {
 | 
			
		||||
export const getList = (data?: Record<string, unknown>) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/admin/order/list",
 | 
			
		||||
    methods: "get",
 | 
			
		||||
    params
 | 
			
		||||
    url: "/api/admin/order/list",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
@@ -8,6 +8,9 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
 | 
			
		||||
export default defineConfig(({ mode }) => {
 | 
			
		||||
  const { VITE_PROXY_BASE_URL, VITE_TARGET_URL } = loadEnv(mode, process.cwd());
 | 
			
		||||
  return {
 | 
			
		||||
    define: {
 | 
			
		||||
      __AUTH_KEY: "'Admin-Authorization'"
 | 
			
		||||
    },
 | 
			
		||||
    plugins: [
 | 
			
		||||
      vue(),
 | 
			
		||||
      vueJsx(),
 | 
			
		||||
 
 | 
			
		||||