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(),
|
||||
|