chore: version 2.10.0

* feat: 权限验证功能

* chore: v2.10.0

* feat: 500 服务异常页面

* feat: 只有结束才会滚动到底部

* chore: 修改 CHANGELOG

* chore: 不存在时输出默认报错
This commit is contained in:
Redon
2023-03-07 22:12:15 +08:00
committed by GitHub
parent a2ffa3cb3a
commit ffd4da91cf
30 changed files with 376 additions and 60 deletions

View File

@@ -33,3 +33,16 @@ export function fetchChatAPIProcess<T = any>(
onDownloadProgress: params.onDownloadProgress,
})
}
export function fetchSession<T>() {
return post<T>({
url: '/session',
})
}
export function fetchVerify<T>(token: string) {
return post<T>({
url: '/verify',
data: { token },
})
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 19 KiB

5
src/icons/403.vue Normal file

File diff suppressed because one or more lines are too long

5
src/icons/500.vue Normal file

File diff suppressed because one or more lines are too long

View File

@@ -12,6 +12,8 @@ export default {
wrong: 'Something went wrong, please try again later.',
success: 'Success',
failed: 'Failed',
verify: 'Verify',
unauthorizedTips: 'Unauthorized, please verify first.',
},
chat: {
placeholder: 'Ask me anything...(Shift + Enter = line break)',

View File

@@ -12,6 +12,8 @@ export default {
wrong: '好像出错了,请稍后再试。',
success: '操作成功',
failed: '操作失败',
verify: '验证',
unauthorizedTips: '未经授权,请先进行验证。',
},
chat: {
placeholder: '来说点什么...Shift + Enter = 换行)',

View File

@@ -12,6 +12,8 @@ export default {
wrong: '好像出錯了,請稍後再試。',
success: '操作成功',
failed: '操作失敗',
verify: '驗證',
unauthorizedTips: '未經授權,請先進行驗證。',
},
chat: {
placeholder: '來講點什麼...Shift + Enter = 換行)',

View File

@@ -1,6 +1,7 @@
import type { App } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import { setupPageGuard } from './permission'
import { ChatLayout } from '@/views/chat/layout'
const routes: RouteRecordRaw[] = [
@@ -18,18 +19,18 @@ const routes: RouteRecordRaw[] = [
],
},
{
path: '/403',
name: '403',
component: () => import('@/views/exception/403/index.vue'),
},
{
path: '/404',
name: '404',
component: () => import('@/views/exception/404/index.vue'),
},
{
path: '/500',
name: '500',
component: () => import('@/views/exception/500/index.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'notFound',
@@ -43,6 +44,8 @@ export const router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
})
setupPageGuard(router)
export async function setupRouter(app: App) {
app.use(router)
await router.isReady()

25
src/router/permission.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { Router } from 'vue-router'
import { useAuthStoreWithout } from '@/store/modules/auth'
export function setupPageGuard(router: Router) {
router.beforeEach(async (from, to, next) => {
const authStore = useAuthStoreWithout()
if (!authStore.session) {
try {
const data = await authStore.getSession()
if (String(data.auth) === 'false' && authStore.token)
authStore.removeToken()
next()
}
catch (error) {
if (from.path !== '/500')
next({ name: '500' })
else
next()
}
}
else {
next()
}
})
}

View File

@@ -0,0 +1,15 @@
import { ss } from '@/utils/storage'
const LOCAL_NAME = 'SECRET_TOKEN'
export function getToken() {
return ss.get(LOCAL_NAME)
}
export function setToken(token: string) {
return ss.set(LOCAL_NAME, token)
}
export function removeToken() {
return ss.remove(LOCAL_NAME)
}

View File

@@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { getToken, removeToken, setToken } from './helper'
import { store } from '@/store'
import { fetchSession } from '@/api'
export interface AuthState {
token: string | undefined
session: { auth: boolean } | null
}
export const useAuthStore = defineStore('auth-store', {
state: (): AuthState => ({
token: getToken(),
session: null,
}),
actions: {
async getSession() {
try {
const { data } = await fetchSession<{ auth: boolean }>()
this.session = { ...data }
return Promise.resolve(data)
}
catch (error) {
return Promise.reject(error)
}
},
setToken(token: string) {
this.token = token
setToken(token)
},
removeToken() {
this.token = undefined
removeToken()
},
},
})
export function useAuthStoreWithout() {
return useAuthStore(store)
}

View File

@@ -1,3 +1,4 @@
export * from './app'
export * from './chat'
export * from './user'
export * from './auth'

View File

@@ -1,4 +1,5 @@
import axios, { type AxiosResponse } from 'axios'
import { useAuthStore } from '@/store'
const service = axios.create({
baseURL: import.meta.env.VITE_GLOB_API_URL,
@@ -6,6 +7,9 @@ const service = axios.create({
service.interceptors.request.use(
(config) => {
const token = useAuthStore().token
if (token)
config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => {

View File

@@ -1,5 +1,6 @@
import type { AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from 'axios'
import request from './axios'
import { useAuthStore } from '@/store'
export interface HttpOption {
url: string
@@ -22,9 +23,16 @@ function http<T = any>(
{ url, data, method, headers, onDownloadProgress, signal, beforeRequest, afterRequest }: HttpOption,
) {
const successHandler = (res: AxiosResponse<Response<T>>) => {
const authStore = useAuthStore()
if (res.data.status === 'Success' || typeof res.data === 'string')
return res.data
if (res.data.status === 'Unauthorized') {
authStore.removeToken()
window.location.reload()
}
return Promise.reject(res.data)
}

View File

@@ -113,13 +113,13 @@ async function onConversation() {
requestOptions: { prompt: message, options: { ...options } },
},
)
scrollToBottom()
}
catch (error) {
//
}
},
})
scrollToBottom()
}
catch (error: any) {
const errorMessage = error?.message ?? t('common.wrong')

View File

@@ -4,12 +4,14 @@ import { NLayout, NLayoutContent } from 'naive-ui'
import { useRouter } from 'vue-router'
import Sider from './sider/index.vue'
import Header from './header/index.vue'
import Permission from './Permission.vue'
import { useBasicLayout } from '@/hooks/useBasicLayout'
import { useAppStore, useChatStore } from '@/store'
import { useAppStore, useAuthStore, useChatStore } from '@/store'
const router = useRouter()
const appStore = useAppStore()
const chatStore = useChatStore()
const authStore = useAuthStore()
router.replace({ name: 'Chat', params: { uuid: chatStore.active } })
@@ -17,6 +19,8 @@ const { isMobile } = useBasicLayout()
const collapsed = computed(() => appStore.siderCollapsed)
const needPermission = computed(() => !!authStore.session?.auth && !authStore.token)
const getMobileClass = computed(() => {
if (isMobile.value)
return ['rounded-none', 'shadow-none']
@@ -44,5 +48,6 @@ const getContainerClass = computed(() => {
</NLayoutContent>
</NLayout>
</div>
<Permission :visible="needPermission" />
</div>
</template>

View File

@@ -0,0 +1,81 @@
<script setup lang='ts'>
import { computed, ref } from 'vue'
import { NButton, NInput, NModal, useMessage } from 'naive-ui'
import { fetchVerify } from '@/api'
import { useAuthStore } from '@/store'
import Icon403 from '@/icons/403.vue'
interface Props {
visible: boolean
}
defineProps<Props>()
const authStore = useAuthStore()
const ms = useMessage()
const loading = ref(false)
const token = ref('')
const disabled = computed(() => !token.value.trim() || loading.value)
async function handleVerify() {
const secretKey = token.value.trim()
if (!secretKey)
return
try {
loading.value = true
await fetchVerify(secretKey)
authStore.setToken(secretKey)
ms.success('success')
window.location.reload()
}
catch (error: any) {
ms.error(error.message ?? 'error')
authStore.removeToken()
token.value = ''
}
finally {
loading.value = false
}
}
function handlePress(event: KeyboardEvent) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleVerify()
}
}
</script>
<template>
<NModal :show="visible" style="width: 90%; max-width: 640px">
<div class="p-10 bg-white rounded dark:bg-slate-800">
<div class="space-y-4">
<header class="space-y-2">
<h2 class="text-2xl font-bold text-center text-slate-800 dark:text-neutral-200">
403
</h2>
<p class="text-base text-center text-slate-500 dark:text-slate-500">
{{ $t('common.unauthorizedTips') }}
</p>
<Icon403 class="w-[200px] m-auto" />
</header>
<NInput v-model:value="token" type="text" placeholder="" @keypress="handlePress" />
<NButton
block
type="primary"
:disabled="disabled"
:loading="loading"
@click="handleVerify"
>
{{ $t('common.verify') }}
</NButton>
</div>
</div>
</NModal>
</template>

View File

@@ -1,34 +0,0 @@
<script lang="ts" setup>
import { NButton } from 'naive-ui'
import { useRouter } from 'vue-router'
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<template>
<div class="flex h-full">
<div class="px-4 m-auto space-y-4 text-center max-[400px]">
<h1 class="text-4xl text-slate-800 dark:text-neutral-200">
No permission
</h1>
<p class="text-base text-slate-500 dark:text-neutral-400">
The page you're trying access has restricted access.
Please refer to your system administrator
</p>
<div class="flex items-center justify-center text-center">
<div class="w-[300px]">
<div class="w-[300px]">
<img src="../../../icons/403.svg" alt="404">
</div>
</div>
</div>
<NButton type="primary" @click="goHome">
Go to Home
</NButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script lang="ts" setup>
import { NButton } from 'naive-ui'
import { useRouter } from 'vue-router'
import Icon500 from '@/icons/500.vue'
const router = useRouter()
function goHome() {
router.push('/')
}
</script>
<template>
<div class="flex h-full dark:bg-neutral-800">
<div class="px-4 m-auto space-y-4 text-center max-[400px]">
<header class="space-y-2">
<h2 class="text-2xl font-bold text-center text-slate-800 dark:text-neutral-200">
500
</h2>
<p class="text-base text-center text-slate-500 dark:text-slate-500">
Server error
</p>
<div class="flex items-center justify-center text-center">
<Icon500 class="w-[300px]" />
</div>
</header>
<NButton type="primary" @click="goHome">
Go to Home
</NButton>
</div>
</div>
</template>