feat(ui): 新增登录
8
gpt-vue/projects/vue-admin/env.d.ts
vendored
@ -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;
|
@ -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>
|
||||||
|
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>
|
<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>
|
||||||
|
@ -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;
|
||||||
|
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()
|
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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 { 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
|
||||||
|
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";
|
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
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -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(),
|
||||||
|