mirror of
https://github.com/Chanzhaoyu/chatgpt-web.git
synced 2025-08-03 20:57:55 +00:00
Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
79dd6c5e18 | ||
![]() |
da75ca944c | ||
![]() |
4458e744cc | ||
![]() |
d2ae2c4f54 | ||
![]() |
cf8e2dd7b6 | ||
![]() |
701ef0e6e1 | ||
![]() |
de34af8747 | ||
![]() |
33c02cfe10 | ||
![]() |
b03f804e35 | ||
![]() |
b6e5c59a9c |
@@ -1,3 +1,10 @@
|
|||||||
|
## v2.2.0
|
||||||
|
|
||||||
|
`2023-02-14`
|
||||||
|
### Feature
|
||||||
|
- 会话和上下文本地储存
|
||||||
|
- 侧边栏本地储存
|
||||||
|
|
||||||
## v2.1.0
|
## v2.1.0
|
||||||
|
|
||||||
`2023-02-14`
|
`2023-02-14`
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chatgpt-web",
|
"name": "chatgpt-web",
|
||||||
"version": "2.1.0",
|
"version": "2.2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "ChatGPT Web",
|
"description": "ChatGPT Web",
|
||||||
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"naive-ui": "^2.34.3",
|
"naive-ui": "^2.34.3",
|
||||||
|
"pinia": "^2.0.30",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
"vue-router": "^4.1.6"
|
"vue-router": "^4.1.6"
|
||||||
},
|
},
|
||||||
@@ -31,10 +32,12 @@
|
|||||||
"@commitlint/cli": "^17.4.3",
|
"@commitlint/cli": "^17.4.3",
|
||||||
"@commitlint/config-conventional": "^17.4.3",
|
"@commitlint/config-conventional": "^17.4.3",
|
||||||
"@iconify/vue": "^4.1.0",
|
"@iconify/vue": "^4.1.0",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/node": "^18.13.0",
|
"@types/node": "^18.13.0",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"axios": "^1.3.2",
|
"axios": "^1.3.2",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"lint-staged": "^13.1.1",
|
"lint-staged": "^13.1.1",
|
||||||
|
48
pnpm-lock.yaml
generated
48
pnpm-lock.yaml
generated
@@ -5,15 +5,18 @@ specifiers:
|
|||||||
'@commitlint/cli': ^17.4.3
|
'@commitlint/cli': ^17.4.3
|
||||||
'@commitlint/config-conventional': ^17.4.3
|
'@commitlint/config-conventional': ^17.4.3
|
||||||
'@iconify/vue': ^4.1.0
|
'@iconify/vue': ^4.1.0
|
||||||
|
'@types/crypto-js': ^4.1.1
|
||||||
'@types/node': ^18.13.0
|
'@types/node': ^18.13.0
|
||||||
'@vitejs/plugin-vue': ^4.0.0
|
'@vitejs/plugin-vue': ^4.0.0
|
||||||
autoprefixer: ^10.4.13
|
autoprefixer: ^10.4.13
|
||||||
axios: ^1.3.2
|
axios: ^1.3.2
|
||||||
|
crypto-js: ^4.1.1
|
||||||
eslint: ^8.34.0
|
eslint: ^8.34.0
|
||||||
husky: ^8.0.3
|
husky: ^8.0.3
|
||||||
lint-staged: ^13.1.1
|
lint-staged: ^13.1.1
|
||||||
naive-ui: ^2.34.3
|
naive-ui: ^2.34.3
|
||||||
npm-run-all: ^4.1.5
|
npm-run-all: ^4.1.5
|
||||||
|
pinia: ^2.0.30
|
||||||
postcss: ^8.4.21
|
postcss: ^8.4.21
|
||||||
rimraf: ^4.1.2
|
rimraf: ^4.1.2
|
||||||
tailwindcss: ^3.2.6
|
tailwindcss: ^3.2.6
|
||||||
@@ -25,6 +28,7 @@ specifiers:
|
|||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
naive-ui: 2.34.3_vue@3.2.47
|
naive-ui: 2.34.3_vue@3.2.47
|
||||||
|
pinia: 2.0.30_hmuptsblhheur2tugfgucj7gc4
|
||||||
vue: 3.2.47
|
vue: 3.2.47
|
||||||
vue-router: 4.1.6_vue@3.2.47
|
vue-router: 4.1.6_vue@3.2.47
|
||||||
|
|
||||||
@@ -33,10 +37,12 @@ devDependencies:
|
|||||||
'@commitlint/cli': 17.4.3
|
'@commitlint/cli': 17.4.3
|
||||||
'@commitlint/config-conventional': 17.4.3
|
'@commitlint/config-conventional': 17.4.3
|
||||||
'@iconify/vue': 4.1.0_vue@3.2.47
|
'@iconify/vue': 4.1.0_vue@3.2.47
|
||||||
|
'@types/crypto-js': 4.1.1
|
||||||
'@types/node': 18.13.0
|
'@types/node': 18.13.0
|
||||||
'@vitejs/plugin-vue': 4.0.0_vite@4.1.1+vue@3.2.47
|
'@vitejs/plugin-vue': 4.0.0_vite@4.1.1+vue@3.2.47
|
||||||
autoprefixer: 10.4.13_postcss@8.4.21
|
autoprefixer: 10.4.13_postcss@8.4.21
|
||||||
axios: 1.3.2
|
axios: 1.3.2
|
||||||
|
crypto-js: 4.1.1
|
||||||
eslint: 8.34.0
|
eslint: 8.34.0
|
||||||
husky: 8.0.3
|
husky: 8.0.3
|
||||||
lint-staged: 13.1.1
|
lint-staged: 13.1.1
|
||||||
@@ -695,6 +701,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==}
|
resolution: {integrity: sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/crypto-js/4.1.1:
|
||||||
|
resolution: {integrity: sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/json-schema/7.0.11:
|
/@types/json-schema/7.0.11:
|
||||||
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -1527,6 +1537,10 @@ packages:
|
|||||||
which: 2.0.2
|
which: 2.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/crypto-js/4.1.1:
|
||||||
|
resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/css-render/0.15.12:
|
/css-render/0.15.12:
|
||||||
resolution: {integrity: sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==}
|
resolution: {integrity: sha512-eWzS66patiGkTTik+ipO9qNGZ+uNuGyTmnz6/+EJIiFg8+3yZRpnMwgFo8YdXhQRsiePzehnusrxVvugNjXzbw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3567,6 +3581,24 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pinia/2.0.30_hmuptsblhheur2tugfgucj7gc4:
|
||||||
|
resolution: {integrity: sha512-q6DUmxWwe/mQgg+55QQjykpKC+aGeGdaJV3niminl19V08dE+LRTvSEuqi6/NLSGCKHI49KGL6tMNEOssFiMyA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.4.0
|
||||||
|
typescript: '>=4.4.4'
|
||||||
|
vue: ^2.6.14 || ^3.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@vue/devtools-api': 6.5.0
|
||||||
|
typescript: 4.9.5
|
||||||
|
vue: 3.2.47
|
||||||
|
vue-demi: 0.13.11_vue@3.2.47
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pluralize/8.0.0:
|
/pluralize/8.0.0:
|
||||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4284,7 +4316,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
|
||||||
engines: {node: '>=4.2.0'}
|
engines: {node: '>=4.2.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/unbox-primitive/1.0.2:
|
/unbox-primitive/1.0.2:
|
||||||
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
@@ -4390,6 +4421,21 @@ packages:
|
|||||||
vue: 3.2.47
|
vue: 3.2.47
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/vue-demi/0.13.11_vue@3.2.47:
|
||||||
|
resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
hasBin: true
|
||||||
|
requiresBuild: true
|
||||||
|
peerDependencies:
|
||||||
|
'@vue/composition-api': ^1.0.0-rc.1
|
||||||
|
vue: ^3.0.0-0 || ^2.6.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@vue/composition-api':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
vue: 3.2.47
|
||||||
|
dev: false
|
||||||
|
|
||||||
/vue-eslint-parser/9.1.0_eslint@8.34.0:
|
/vue-eslint-parser/9.1.0_eslint@8.34.0:
|
||||||
resolution: {integrity: sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==}
|
resolution: {integrity: sha512-NGn/iQy8/Wb7RrRa4aRkokyCZfOUWk19OP5HP6JEozQFX5AoS/t+Z0ZN7FY4LlmWc4FNI922V7cvX28zctN8dQ==}
|
||||||
engines: {node: ^14.17.0 || >=16.0.0}
|
engines: {node: ^14.17.0 || >=16.0.0}
|
||||||
|
@@ -1,17 +1,16 @@
|
|||||||
|
import type { GenericAbortSignal } from 'axios'
|
||||||
import { post } from '@/utils/request'
|
import { post } from '@/utils/request'
|
||||||
|
|
||||||
|
export const controller = new AbortController()
|
||||||
|
|
||||||
export function fetchChatAPI<T = any>(
|
export function fetchChatAPI<T = any>(
|
||||||
prompt: string,
|
prompt: string,
|
||||||
options?: { conversationId?: string; parentMessageId?: string },
|
options?: { conversationId?: string; parentMessageId?: string },
|
||||||
|
signal?: GenericAbortSignal,
|
||||||
) {
|
) {
|
||||||
return post<T>({
|
return post<T>({
|
||||||
url: '/chat',
|
url: '/chat',
|
||||||
data: { prompt, options },
|
data: { prompt, options },
|
||||||
})
|
signal,
|
||||||
}
|
|
||||||
|
|
||||||
export function clearConversations<T = any>() {
|
|
||||||
return post<T>({
|
|
||||||
url: '/clear',
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
28
src/components/business/Chat/hooks/useChat.ts
Normal file
28
src/components/business/Chat/hooks/useChat.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { useHistoryStore } from '@/store'
|
||||||
|
|
||||||
|
export function useChat() {
|
||||||
|
const historyStore = useHistoryStore()
|
||||||
|
|
||||||
|
function addChat(
|
||||||
|
message: string,
|
||||||
|
args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions },
|
||||||
|
uuid?: number | null,
|
||||||
|
) {
|
||||||
|
historyStore.addChat(
|
||||||
|
{
|
||||||
|
dateTime: new Date().toLocaleString(),
|
||||||
|
message,
|
||||||
|
reversal: args?.reversal ?? false,
|
||||||
|
error: args?.error ?? false,
|
||||||
|
options: args?.options ?? undefined,
|
||||||
|
},
|
||||||
|
uuid,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearChat() {
|
||||||
|
historyStore.clearChat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { addChat, clearChat }
|
||||||
|
}
|
@@ -1,27 +1,31 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { NButton, NInput, useMessage } from 'naive-ui'
|
import { NButton, NInput, useMessage } from 'naive-ui'
|
||||||
import type { ChatOptions, ChatProps } from './types'
|
|
||||||
import { Message } from './components'
|
import { Message } from './components'
|
||||||
import { Layout } from './layout'
|
import { Layout } from './layout'
|
||||||
|
import { useChat } from './hooks/useChat'
|
||||||
import { fetchChatAPI } from '@/api'
|
import { fetchChatAPI } from '@/api'
|
||||||
import { HoverButton, SvgIcon } from '@/components/common'
|
import { HoverButton, SvgIcon } from '@/components/common'
|
||||||
|
import { useHistoryStore } from '@/store'
|
||||||
|
import { isNumber } from '@/utils/is'
|
||||||
|
|
||||||
|
let controller = new AbortController()
|
||||||
|
|
||||||
|
const ms = useMessage()
|
||||||
|
|
||||||
|
const historyStore = useHistoryStore()
|
||||||
|
|
||||||
const scrollRef = ref<HTMLDivElement>()
|
const scrollRef = ref<HTMLDivElement>()
|
||||||
|
|
||||||
const ms = useMessage()
|
const { addChat, clearChat: handleClear } = useChat()
|
||||||
|
|
||||||
const prompt = ref('')
|
const prompt = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const list = ref<ChatProps[]>([])
|
const currentActive = computed(() => historyStore.active)
|
||||||
const chatList = computed(() => list.value.filter(item => (!item.reversal && !item.error)))
|
|
||||||
|
|
||||||
function initChat() {
|
const list = computed<Chat.Chat[]>(() => historyStore.getCurrentChat)
|
||||||
addMessage('Hi, I am ChatGPT, a chatbot based on GPT-3.')
|
const chatList = computed<Chat.Chat[]>(() => list.value.filter(item => (!item.reversal && !item.error)))
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(initChat)
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (loading.value)
|
if (loading.value)
|
||||||
@@ -37,7 +41,7 @@ async function handleSubmit() {
|
|||||||
addMessage(message, { reversal: true })
|
addMessage(message, { reversal: true })
|
||||||
prompt.value = ''
|
prompt.value = ''
|
||||||
|
|
||||||
let options: ChatOptions = {}
|
let options: Chat.ChatOptions = {}
|
||||||
const lastContext = chatList.value[chatList.value.length - 1]?.options
|
const lastContext = chatList.value[chatList.value.length - 1]?.options
|
||||||
|
|
||||||
if (lastContext)
|
if (lastContext)
|
||||||
@@ -45,10 +49,11 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const { data } = await fetchChatAPI(message, options)
|
const { data } = await fetchChatAPI(message, options, controller.signal)
|
||||||
addMessage(data?.text ?? '', { options: { conversationId: data.conversationId, parentMessageId: data.id } })
|
addMessage(data?.text ?? '', { options: { conversationId: data.conversationId, parentMessageId: data.id } })
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
|
if (error.message !== 'cancelled')
|
||||||
addMessage(`Error: ${error.message ?? 'Request failed, please try again later.'}`, { error: true })
|
addMessage(`Error: ${error.message ?? 'Request failed, please try again later.'}`, { error: true })
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -63,22 +68,37 @@ function handleEnter(event: KeyboardEvent) {
|
|||||||
|
|
||||||
function addMessage(
|
function addMessage(
|
||||||
message: string,
|
message: string,
|
||||||
args?: { reversal?: boolean; error?: boolean; options?: ChatOptions },
|
args?: { reversal?: boolean; error?: boolean; options?: Chat.ChatOptions },
|
||||||
|
uuid?: number | null,
|
||||||
) {
|
) {
|
||||||
list.value.push({
|
addChat(message, args, uuid)
|
||||||
dateTime: new Date().toLocaleString(),
|
scrollToBottom()
|
||||||
message,
|
}
|
||||||
reversal: args?.reversal ?? false,
|
|
||||||
error: args?.error ?? false,
|
function scrollToBottom() {
|
||||||
options: args?.options ?? undefined,
|
|
||||||
})
|
|
||||||
nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight))
|
nextTick(() => scrollRef.value && (scrollRef.value.scrollTop = scrollRef.value.scrollHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClear() {
|
function handleCancel() {
|
||||||
list.value = []
|
// 取消之后一定要重新赋值,否则会报错
|
||||||
setTimeout(initChat, 100)
|
controller.abort()
|
||||||
|
controller = new AbortController()
|
||||||
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
scrollToBottom()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
currentActive,
|
||||||
|
(active) => {
|
||||||
|
if (isNumber(active)) {
|
||||||
|
handleCancel()
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -96,13 +116,13 @@ function handleClear() {
|
|||||||
</main>
|
</main>
|
||||||
<footer class="p-4">
|
<footer class="p-4">
|
||||||
<div class="flex items-center justify-between space-x-2">
|
<div class="flex items-center justify-between space-x-2">
|
||||||
<HoverButton tooltip="Clear conversations" @click="handleClear">
|
<HoverButton tooltip="Clear conversations">
|
||||||
<span class="text-xl text-[#4f555e]">
|
<span class="text-xl text-[#4f555e]" @click="handleClear">
|
||||||
<SvgIcon icon="ri:delete-bin-line" />
|
<SvgIcon icon="ri:delete-bin-line" />
|
||||||
</span>
|
</span>
|
||||||
</HoverButton>
|
</HoverButton>
|
||||||
<NInput v-model:value="prompt" placeholder="Type a message..." @keypress="handleEnter" />
|
<NInput v-model:value="prompt" placeholder="Type a message..." @keypress="handleEnter" />
|
||||||
<NButton type="primary" :loading="loading" @click="handleSubmit">
|
<NButton type="primary" :loading="loading" @click="handleCancel">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SvgIcon icon="ri:send-plane-fill" />
|
<SvgIcon icon="ri:send-plane-fill" />
|
||||||
</template>
|
</template>
|
||||||
|
@@ -3,9 +3,8 @@ import { HoverButton, SvgIcon, UserAvatar } from '@/components/common'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="flex items-center justify-between p-4 overflow-hidden border-t">
|
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t h-[70px]">
|
||||||
<UserAvatar />
|
<UserAvatar class="flex-1" />
|
||||||
|
|
||||||
<HoverButton tooltip="Setting">
|
<HoverButton tooltip="Setting">
|
||||||
<span class="text-xl text-[#4f555e]">
|
<span class="text-xl text-[#4f555e]">
|
||||||
<SvgIcon icon="ri:settings-4-line" />
|
<SvgIcon icon="ri:settings-4-line" />
|
||||||
|
@@ -1,14 +1,67 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { NScrollbar } from 'naive-ui'
|
import { ref } from 'vue'
|
||||||
import ListItem from './ListItem.vue'
|
import { NInput, NScrollbar } from 'naive-ui'
|
||||||
|
import { SvgIcon } from '@/components/common'
|
||||||
|
import { useHistoryStore } from '@/store'
|
||||||
|
|
||||||
|
const historyStore = useHistoryStore()
|
||||||
|
|
||||||
|
const dataSources = ref(historyStore.historyChat)
|
||||||
|
|
||||||
|
function handleSelect(index: number) {
|
||||||
|
historyStore.chooseHistory(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEdit(index: number, isEdit: boolean) {
|
||||||
|
historyStore.editHistory(index, isEdit)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRemove(index: number) {
|
||||||
|
historyStore.removeHistory(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEnter(index: number, isEdit: boolean, event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter')
|
||||||
|
handleEdit(index, isEdit)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NScrollbar class="px-4">
|
<NScrollbar class="px-4">
|
||||||
<div class="flex flex-col gap-2 text-sm">
|
<div class="flex flex-col gap-2 text-sm">
|
||||||
<ListItem text="Learning correlation" />
|
<div v-for="(item, index) of dataSources" :key="index">
|
||||||
<ListItem text="Write Code" />
|
<a
|
||||||
<ListItem text="docs..." />
|
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer pr-14 hover:bg-neutral-100 group"
|
||||||
|
:class="historyStore.active === index && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]']"
|
||||||
|
@click="handleSelect(index)"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<SvgIcon icon="ri:message-3-line" />
|
||||||
|
</span>
|
||||||
|
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap">
|
||||||
|
<NInput
|
||||||
|
v-if="item.isEdit" v-model:value="item.title" size="tiny"
|
||||||
|
@keypress="handleEnter(index, false, $event)"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ item.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="absolute z-10 flex visible right-1">
|
||||||
|
<template v-if="item.isEdit">
|
||||||
|
<button class="p-1" @click="handleEdit(index, false)">
|
||||||
|
<SvgIcon icon="ri:save-line" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button class="p-1">
|
||||||
|
<SvgIcon icon="ri:edit-line" @click="handleEdit(index, true)" />
|
||||||
|
</button>
|
||||||
|
<button class="p-1" @click="handleRemove(index)">
|
||||||
|
<SvgIcon icon="ri:delete-bin-line" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -1,52 +0,0 @@
|
|||||||
<script setup lang='ts'>
|
|
||||||
import { SvgIcon } from '@/components/common'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
text: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Emit {
|
|
||||||
(e: 'click',): void
|
|
||||||
(e: 'edit',): void
|
|
||||||
(e: 'delete',): void
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<Props>()
|
|
||||||
|
|
||||||
const emit = defineEmits<Emit>()
|
|
||||||
|
|
||||||
function handleClick(event: Event) {
|
|
||||||
emit('click')
|
|
||||||
event.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEdit() {
|
|
||||||
emit('edit')
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDelete() {
|
|
||||||
emit('delete')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<a
|
|
||||||
class="relative flex items-center gap-3 px-3 py-3 break-all rounded-md cursor-pointer bg-neutral-50 pr-14 hover:bg-neutral-100 group"
|
|
||||||
@click="handleClick"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
<SvgIcon icon="ri:message-3-line" />
|
|
||||||
</span>
|
|
||||||
<div class="relative flex-1 overflow-hidden break-all text-ellipsis whitespace-nowrap max-h-5">
|
|
||||||
<span>{{ text }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="absolute z-10 flex visible right-1">
|
|
||||||
<button class="p-1" @click="handleEdit">
|
|
||||||
<SvgIcon icon="ri:edit-line" />
|
|
||||||
</button>
|
|
||||||
<button class="p-1" @click="handleDelete">
|
|
||||||
<SvgIcon icon="ri:delete-bin-line" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
@@ -1,42 +1,26 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { NButton, NLayoutSider, useMessage } from 'naive-ui'
|
import { NButton, NLayoutSider } from 'naive-ui'
|
||||||
import List from './List.vue'
|
import List from './List.vue'
|
||||||
import Footer from './Footer.vue'
|
import Footer from './Footer.vue'
|
||||||
|
import { useAppStore, useHistoryStore } from '@/store'
|
||||||
|
|
||||||
interface Props {
|
const appStore = useAppStore()
|
||||||
collapsed?: boolean
|
const historyStore = useHistoryStore()
|
||||||
}
|
|
||||||
|
|
||||||
interface Emit {
|
const collapsed = ref(appStore.siderCollapsed ?? false)
|
||||||
(e: 'update:collapsed', value: boolean): void
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
|
||||||
collapsed: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<Emit>()
|
|
||||||
|
|
||||||
const ms = useMessage()
|
|
||||||
|
|
||||||
const collapsed = ref(props.collapsed)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.collapsed,
|
|
||||||
(value: boolean) => {
|
|
||||||
collapsed.value = value
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
function handleAdd() {
|
function handleAdd() {
|
||||||
ms.info('Coming soon...')
|
historyStore.addHistory({
|
||||||
|
title: 'New Chat',
|
||||||
|
isEdit: false,
|
||||||
|
data: [],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCollapsed() {
|
function handleCollapsed() {
|
||||||
collapsed.value = !collapsed.value
|
collapsed.value = !collapsed.value
|
||||||
emit('update:collapsed', collapsed.value)
|
appStore.setSiderCollapsed(collapsed.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
export interface ChatOptions {
|
|
||||||
conversationId?: string
|
|
||||||
parentMessageId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChatProps {
|
|
||||||
dateTime: string
|
|
||||||
message: string
|
|
||||||
reversal?: boolean
|
|
||||||
error?: boolean
|
|
||||||
options?: ChatOptions
|
|
||||||
}
|
|
18
src/main.ts
18
src/main.ts
@@ -1,21 +1,17 @@
|
|||||||
import './styles/global.css'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
import { setupAssets } from '@/plugins'
|
||||||
|
import { setupStore } from '@/store'
|
||||||
import { setupRouter } from '@/router'
|
import { setupRouter } from '@/router'
|
||||||
|
|
||||||
/** Tailwind's Preflight Style Override */
|
|
||||||
function naiveStyleOverride() {
|
|
||||||
const meta = document.createElement('meta')
|
|
||||||
meta.name = 'naive-ui-style'
|
|
||||||
document.head.appendChild(meta)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Setup */
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
naiveStyleOverride()
|
setupAssets()
|
||||||
|
|
||||||
|
setupStore(app)
|
||||||
|
|
||||||
await setupRouter(app)
|
await setupRouter(app)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
14
src/plugins/assets.ts
Normal file
14
src/plugins/assets.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import '@/styles/global.css'
|
||||||
|
|
||||||
|
/** Tailwind's Preflight Style Override */
|
||||||
|
function naiveStyleOverride() {
|
||||||
|
const meta = document.createElement('meta')
|
||||||
|
meta.name = 'naive-ui-style'
|
||||||
|
document.head.appendChild(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAssets() {
|
||||||
|
naiveStyleOverride()
|
||||||
|
}
|
||||||
|
|
||||||
|
export default setupAssets
|
3
src/plugins/index.ts
Normal file
3
src/plugins/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import setupAssets from './assets'
|
||||||
|
|
||||||
|
export { setupAssets }
|
9
src/store/index.ts
Normal file
9
src/store/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import type { App } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
export function setupStore(app: App) {
|
||||||
|
const store = createPinia()
|
||||||
|
app.use(store)
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from './modules'
|
20
src/store/modules/app/helper.ts
Normal file
20
src/store/modules/app/helper.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ss } from '@/utils/storage'
|
||||||
|
|
||||||
|
const LOCAL_NAME = 'appSetting'
|
||||||
|
|
||||||
|
export interface AppState {
|
||||||
|
siderCollapsed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultSetting() {
|
||||||
|
return { siderCollapsed: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalSetting() {
|
||||||
|
const localSetting: AppState | undefined = ss.get(LOCAL_NAME)
|
||||||
|
return localSetting ?? defaultSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalSetting(setting: AppState) {
|
||||||
|
ss.set(LOCAL_NAME, setting)
|
||||||
|
}
|
16
src/store/modules/app/index.ts
Normal file
16
src/store/modules/app/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { AppState } from './helper'
|
||||||
|
import { getLocalSetting, setLocalSetting } from './helper'
|
||||||
|
|
||||||
|
export const useAppStore = defineStore('app-store', {
|
||||||
|
state: (): AppState => getLocalSetting(),
|
||||||
|
actions: {
|
||||||
|
setSiderCollapsed(collapsed: boolean) {
|
||||||
|
this.siderCollapsed = collapsed
|
||||||
|
setLocalSetting(this.$state)
|
||||||
|
},
|
||||||
|
toggleSiderCollapse() {
|
||||||
|
this.setSiderCollapsed(!this.siderCollapsed)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
21
src/store/modules/history/helper.ts
Normal file
21
src/store/modules/history/helper.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ss } from '@/utils/storage'
|
||||||
|
|
||||||
|
const LOCAL_NAME = 'historyChat'
|
||||||
|
|
||||||
|
export interface HistoryState {
|
||||||
|
historyChat: Chat.HistoryChat[]
|
||||||
|
active: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultSetting() {
|
||||||
|
return { historyChat: [], active: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLocalHistory() {
|
||||||
|
const localSetting: HistoryState | undefined = ss.get(LOCAL_NAME)
|
||||||
|
return localSetting ?? defaultSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalHistory(data: HistoryState) {
|
||||||
|
ss.set(LOCAL_NAME, data)
|
||||||
|
}
|
73
src/store/modules/history/index.ts
Normal file
73
src/store/modules/history/index.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { HistoryState } from './helper'
|
||||||
|
import { getLocalHistory, setLocalHistory } from './helper'
|
||||||
|
export const useHistoryStore = defineStore('history-store', {
|
||||||
|
state: (): HistoryState => getLocalHistory(),
|
||||||
|
getters: {
|
||||||
|
getCurrentHistory(state): Chat.HistoryChat {
|
||||||
|
if (state.historyChat.length) {
|
||||||
|
if (state.active === null || state.active >= state.historyChat.length || state.active < 0)
|
||||||
|
state.active = 0
|
||||||
|
return state.historyChat[state.active] ?? { title: '', isEdit: false, data: [] }
|
||||||
|
}
|
||||||
|
state.active = null
|
||||||
|
return { title: '', isEdit: false, data: [] }
|
||||||
|
},
|
||||||
|
getCurrentChat(): Chat.Chat[] {
|
||||||
|
return this.getCurrentHistory.data ?? []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
addChat(data: Chat.Chat, uuid: number | null = null) {
|
||||||
|
if (this.active === null) {
|
||||||
|
this.historyChat.push({ title: data.message, isEdit: false, data: [data] })
|
||||||
|
this.active = this.historyChat.length - 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const active = uuid !== null ? uuid : this.active
|
||||||
|
if (this.historyChat[active].title === 'New Chat')
|
||||||
|
this.historyChat[active].title = data.message
|
||||||
|
this.historyChat[active].data.push(data)
|
||||||
|
}
|
||||||
|
setLocalHistory(this.$state)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearChat() {
|
||||||
|
if (this.active !== null) {
|
||||||
|
this.historyChat[this.active].data = []
|
||||||
|
setLocalHistory(this.$state)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addHistory(data: Chat.HistoryChat) {
|
||||||
|
this.historyChat.push(data)
|
||||||
|
this.active = this.historyChat.length - 1
|
||||||
|
setLocalHistory(this.$state)
|
||||||
|
},
|
||||||
|
|
||||||
|
editHistory(index: number, isEdit: boolean) {
|
||||||
|
this.historyChat[index].isEdit = isEdit
|
||||||
|
setLocalHistory(this.$state)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeHistory(index: number) {
|
||||||
|
this.historyChat.splice(index, 1)
|
||||||
|
if (this.active === index) {
|
||||||
|
if (this.historyChat.length === 0)
|
||||||
|
this.active = null
|
||||||
|
else if (this.active === this.historyChat.length)
|
||||||
|
this.active--
|
||||||
|
else
|
||||||
|
this.active = 0
|
||||||
|
}
|
||||||
|
setLocalHistory(this.$state)
|
||||||
|
},
|
||||||
|
|
||||||
|
chooseHistory(index: number) {
|
||||||
|
if (this.active === index)
|
||||||
|
return
|
||||||
|
this.active = index
|
||||||
|
setLocalHistory(this.$state)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
2
src/store/modules/index.ts
Normal file
2
src/store/modules/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './app'
|
||||||
|
export * from './history'
|
20
src/typings/chat.d.ts
vendored
Normal file
20
src/typings/chat.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
declare namespace Chat{
|
||||||
|
interface ChatOptions {
|
||||||
|
conversationId?: string
|
||||||
|
parentMessageId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Chat {
|
||||||
|
dateTime: string
|
||||||
|
message: string
|
||||||
|
reversal?: boolean
|
||||||
|
error?: boolean
|
||||||
|
options?: ChatOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryChat {
|
||||||
|
title: string
|
||||||
|
isEdit: boolean
|
||||||
|
data: Chat[]
|
||||||
|
}
|
||||||
|
}
|
18
src/utils/crypto/index.ts
Normal file
18
src/utils/crypto/index.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
|
const CryptoSecret = '__CRYPTO_SECRET__'
|
||||||
|
|
||||||
|
export function enCrypto(data: any) {
|
||||||
|
const str = JSON.stringify(data)
|
||||||
|
return CryptoJS.AES.encrypt(str, CryptoSecret).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deCrypto(data: string) {
|
||||||
|
const bytes = CryptoJS.AES.decrypt(data, CryptoSecret)
|
||||||
|
const str = bytes.toString(CryptoJS.enc.Utf8)
|
||||||
|
|
||||||
|
if (str)
|
||||||
|
return JSON.parse(str)
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
55
src/utils/is/index.ts
Normal file
55
src/utils/is/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
export function isNumber<T extends number>(value: T | unknown): value is number {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Number]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isString<T extends string>(value: T | unknown): value is string {
|
||||||
|
return Object.prototype.toString.call(value) === '[object String]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBoolean<T extends boolean>(value: T | unknown): value is boolean {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Boolean]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNull<T extends null>(value: T | unknown): value is null {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Null]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isUndefine<T extends undefined>(value: T | unknown): value is undefined {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Undefined]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject<T extends object>(value: T | unknown): value is object {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Object]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isArray<T extends any[]>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Array]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFunction<T extends (...args: any[]) => any | void | never>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Function]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDate<T extends Date>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Date]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRegExp<T extends RegExp>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object RegExp]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPromise<T extends Promise<any>>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Promise]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSet<T extends Set<any>>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Set]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMap<T extends Map<any, any>>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object Map]'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFile<T extends File>(value: T | unknown): value is T {
|
||||||
|
return Object.prototype.toString.call(value) === '[object File]'
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import type { AxiosResponse } from 'axios'
|
import type { AxiosResponse, GenericAbortSignal } from 'axios'
|
||||||
import request from './axios'
|
import request from './axios'
|
||||||
|
|
||||||
export interface HttpOption {
|
export interface HttpOption {
|
||||||
@@ -6,6 +6,7 @@ export interface HttpOption {
|
|||||||
data?: any
|
data?: any
|
||||||
method?: string
|
method?: string
|
||||||
headers?: any
|
headers?: any
|
||||||
|
signal?: GenericAbortSignal
|
||||||
beforeRequest?: () => void
|
beforeRequest?: () => void
|
||||||
afterRequest?: () => void
|
afterRequest?: () => void
|
||||||
}
|
}
|
||||||
@@ -20,7 +21,7 @@ export interface Response<T = any> {
|
|||||||
status: string
|
status: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function http<T = any>({ url, data, method, headers, beforeRequest, afterRequest }: HttpOption) {
|
function http<T = any>({ url, data, method, headers, signal, beforeRequest, afterRequest }: HttpOption) {
|
||||||
const successHandler = (res: AxiosResponse<Response<T>>) => {
|
const successHandler = (res: AxiosResponse<Response<T>>) => {
|
||||||
if (res.data.status === 'Success')
|
if (res.data.status === 'Success')
|
||||||
return res.data
|
return res.data
|
||||||
@@ -40,30 +41,32 @@ function http<T = any>({ url, data, method, headers, beforeRequest, afterRequest
|
|||||||
const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
|
const params = Object.assign(typeof data === 'function' ? data() : data ?? {}, {})
|
||||||
|
|
||||||
return method === 'GET'
|
return method === 'GET'
|
||||||
? request.get(url, { params }).then(successHandler, failHandler)
|
? request.get(url, { params, signal }).then(successHandler, failHandler)
|
||||||
: request.post(url, params, { headers }).then(successHandler, failHandler)
|
: request.post(url, params, { headers, signal }).then(successHandler, failHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get<T = any>(
|
export function get<T = any>(
|
||||||
{ url, data, method = 'GET', beforeRequest, afterRequest }: HttpOption,
|
{ url, data, method = 'GET', signal, beforeRequest, afterRequest }: HttpOption,
|
||||||
): Promise<Response<T>> {
|
): Promise<Response<T>> {
|
||||||
return http<T>({
|
return http<T>({
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
data,
|
data,
|
||||||
|
signal,
|
||||||
beforeRequest,
|
beforeRequest,
|
||||||
afterRequest,
|
afterRequest,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function post<T = any>(
|
export function post<T = any>(
|
||||||
{ url, data, method = 'POST', headers, beforeRequest, afterRequest }: HttpOption,
|
{ url, data, method = 'POST', headers, signal, beforeRequest, afterRequest }: HttpOption,
|
||||||
): Promise<Response<T>> {
|
): Promise<Response<T>> {
|
||||||
return http<T>({
|
return http<T>({
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
data,
|
data,
|
||||||
headers,
|
headers,
|
||||||
|
signal,
|
||||||
beforeRequest,
|
beforeRequest,
|
||||||
afterRequest,
|
afterRequest,
|
||||||
})
|
})
|
||||||
|
1
src/utils/storage/index.ts
Normal file
1
src/utils/storage/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './local'
|
70
src/utils/storage/local.ts
Normal file
70
src/utils/storage/local.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { deCrypto, enCrypto } from '../crypto'
|
||||||
|
|
||||||
|
interface StorageData<T = any> {
|
||||||
|
data: T
|
||||||
|
expire: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLocalStorage(options?: { expire?: number | null; crypto?: boolean }) {
|
||||||
|
const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7
|
||||||
|
|
||||||
|
const { expire, crypto } = Object.assign(
|
||||||
|
{
|
||||||
|
expire: DEFAULT_CACHE_TIME,
|
||||||
|
crypto: true,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
|
||||||
|
function set<T = any>(key: string, data: T) {
|
||||||
|
const storageData: StorageData<T> = {
|
||||||
|
data,
|
||||||
|
expire: expire !== null ? new Date().getTime() + expire * 1000 : null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = crypto ? enCrypto(storageData) : JSON.stringify(storageData)
|
||||||
|
window.localStorage.setItem(key, json)
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(key: string) {
|
||||||
|
const json = window.localStorage.getItem(key)
|
||||||
|
if (json) {
|
||||||
|
let storageData: StorageData | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
storageData = crypto ? deCrypto(json) : JSON.parse(json)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
// Prevent failure
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storageData) {
|
||||||
|
const { data, expire } = storageData
|
||||||
|
if (expire === null || expire >= Date.now())
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(key)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(key: string) {
|
||||||
|
window.localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
window.localStorage.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
set,
|
||||||
|
get,
|
||||||
|
remove,
|
||||||
|
clear,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ls = createLocalStorage()
|
||||||
|
|
||||||
|
export const ss = createLocalStorage({ expire: null, crypto: false })
|
Reference in New Issue
Block a user