feat(ui): 新增登录

This commit is contained in:
廖彦棋 2024-03-06 17:54:38 +08:00
parent 5a1a596098
commit ef06f0da98
27 changed files with 287 additions and 21 deletions

View File

@ -1 +1,9 @@
/// <reference types="vite/client" /> /// <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;

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<link rel="icon" href="/favicon.ico"> <link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title> <title>ChatPlus-Ai</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -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"

View File

@ -1,3 +0,0 @@
VITE_PROXY_BASE_URL=""
VITE_TARGET_URL="/"
VITE_SOCKET_IO_URL="/"

View File

@ -1,3 +1,31 @@
<template> <template>
<RouterView /> <RouterView />
</template> </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>

View File

@ -1,15 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { IconDown, IconExport } from "@arco-design/web-vue/es/icon"; 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 SystemMenu from "./SystemMenu.vue";
import PageWrapper from "./PageWrapper.vue"; import PageWrapper from "./PageWrapper.vue";
const logoWidth = "200px"; const logoWidth = "200px";
const authStore = useAuthStore();
</script> </script>
<template> <template>
<ALayout class="custom-layout"> <ALayout class="custom-layout">
<ALayoutHeader class="custom-layout-header"> <ALayoutHeader class="custom-layout-header">
<div class="logo"></div> <div class="logo">
<img :src="Logo" alt="logo" />
<span>ChatPlus 控制台</span>
</div>
<div class="action"> <div class="action">
<ADropdown> <ADropdown>
<ASpace align="center" :size="4"> <ASpace align="center" :size="4">
@ -20,7 +26,11 @@ const logoWidth = "200px";
<ADoption value="changeOwnPwd">更改密码</ADoption> <ADoption value="changeOwnPwd">更改密码</ADoption>
</template> </template>
<template #footer> <template #footer>
<APopconfirm content="确认退出?" position="br"> <APopconfirm
content="确认退出?"
position="br"
@ok="authStore.logout"
>
<ASpace align="center" class="logout-area"> <ASpace align="center" class="logout-area">
<IconExport size="16" /> <IconExport size="16" />
<span>退出</span> <span>退出</span>
@ -52,10 +62,15 @@ const logoWidth = "200px";
align-items: center; align-items: center;
border-bottom: 1px solid var(--color-neutral-2); border-bottom: 1px solid var(--color-neutral-2);
.logo { .logo {
display: block; display: flex;
width: v-bind("logoWidth"); width: v-bind("logoWidth");
text-align: center; align-items: center;
cursor: pointer; justify-content: center;
gap: 12px;
img {
width: 30px;
height: 30px;
}
} }
.action { .action {
display: flex; display: flex;

View 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

View File

@ -7,6 +7,8 @@ export const uploadUrl = import.meta.env.VITE_PROXY_BASE_URL + "/common/upload/m
export const instance = createInstance() export const instance = createInstance()
instance.interceptors.request.use((config) => { instance.interceptors.request.use((config) => {
config.headers[__AUTH_KEY] = localStorage.getItem(__AUTH_KEY);
config.headers["Authorization"] = localStorage.getItem(__AUTH_KEY);
return config; return config;
}); });

View 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",
});
};

View File

@ -1,7 +1,21 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from "@/stores/auth";
import CustomLayout from '@/components/CustomLayout.vue' import CustomLayout from '@/components/CustomLayout.vue'
import menu from './menu' 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({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
@ -12,12 +26,28 @@ const router = createRouter({
redirect: () => menu[0].path, redirect: () => menu[0].path,
children: menu children: menu
}, },
{ ...whiteListRoutes
path: "/:pathMatch(.*)*",
name: "404",
component: () => import("@/views/NotFound.vue"),
},
] ]
}) })
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 export default router

View 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)
}
}
}
})

View 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>

View File

@ -1,9 +1,9 @@
import http from "@/http/config"; import http from "@/http/config";
export const getList = (params?: Record<string, unknown>) => { export const getList = (data?: Record<string, unknown>) => {
return http({ return http({
url: "/admin/order/list", url: "/api/admin/order/list",
methods: "get", method: "post",
params data
}) })
} }

View File

@ -8,6 +8,9 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const { VITE_PROXY_BASE_URL, VITE_TARGET_URL } = loadEnv(mode, process.cwd()); const { VITE_PROXY_BASE_URL, VITE_TARGET_URL } = loadEnv(mode, process.cwd());
return { return {
define: {
__AUTH_KEY: "'Admin-Authorization'"
},
plugins: [ plugins: [
vue(), vue(),
vueJsx(), vueJsx(),