frontend 兼容
This commit is contained in:
20
ai-chat-web/src/components/common/HoverButton/Button.vue
Normal file
20
ai-chat-web/src/components/common/HoverButton/Button.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang='ts'>
|
||||
interface Emit {
|
||||
(e: 'click'): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
function handleClick() {
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center justify-center w-10 h-10 transition rounded-full hover:bg-neutral-100 dark:hover:bg-[#414755]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
46
ai-chat-web/src/components/common/HoverButton/index.vue
Normal file
46
ai-chat-web/src/components/common/HoverButton/index.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed } from 'vue'
|
||||
import type { PopoverPlacement } from 'naive-ui'
|
||||
import { NTooltip } from 'naive-ui'
|
||||
import Button from './Button.vue'
|
||||
|
||||
interface Props {
|
||||
tooltip?: string
|
||||
placement?: PopoverPlacement
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'click'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
tooltip: '',
|
||||
placement: 'bottom',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const showTooltip = computed(() => Boolean(props.tooltip))
|
||||
|
||||
function handleClick() {
|
||||
emit('click')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="showTooltip">
|
||||
<NTooltip :placement="placement" trigger="hover">
|
||||
<template #trigger>
|
||||
<Button @click="handleClick">
|
||||
<slot />
|
||||
</Button>
|
||||
</template>
|
||||
{{ tooltip }}
|
||||
</NTooltip>
|
||||
</div>
|
||||
<div v-else>
|
||||
<Button @click="handleClick">
|
||||
<slot />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
43
ai-chat-web/src/components/common/NaiveProvider/index.vue
Normal file
43
ai-chat-web/src/components/common/NaiveProvider/index.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, h } from 'vue'
|
||||
import {
|
||||
NDialogProvider,
|
||||
NLoadingBarProvider,
|
||||
NMessageProvider,
|
||||
NNotificationProvider,
|
||||
useDialog,
|
||||
useLoadingBar,
|
||||
useMessage,
|
||||
useNotification,
|
||||
} from 'naive-ui'
|
||||
|
||||
function registerNaiveTools() {
|
||||
window.$loadingBar = useLoadingBar()
|
||||
window.$dialog = useDialog()
|
||||
window.$message = useMessage()
|
||||
window.$notification = useNotification()
|
||||
}
|
||||
|
||||
const NaiveProviderContent = defineComponent({
|
||||
name: 'NaiveProviderContent',
|
||||
setup() {
|
||||
registerNaiveTools()
|
||||
},
|
||||
render() {
|
||||
return h('div')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NLoadingBarProvider>
|
||||
<NDialogProvider>
|
||||
<NNotificationProvider>
|
||||
<NMessageProvider>
|
||||
<slot />
|
||||
<NaiveProviderContent />
|
||||
</NMessageProvider>
|
||||
</NNotificationProvider>
|
||||
</NDialogProvider>
|
||||
</NLoadingBarProvider>
|
||||
</template>
|
||||
478
ai-chat-web/src/components/common/PromptStore/index.vue
Normal file
478
ai-chat-web/src/components/common/PromptStore/index.vue
Normal file
@@ -0,0 +1,478 @@
|
||||
<script setup lang='ts'>
|
||||
import type { DataTableColumns } from 'naive-ui'
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { NButton, NCard, NDataTable, NDivider, NInput, NList, NListItem, NModal, NPopconfirm, NSpace, NTabPane, NTabs, NThing, useMessage } from 'naive-ui'
|
||||
import PromptRecommend from '../../../assets/recommend.json'
|
||||
import { SvgIcon } from '..'
|
||||
import { usePromptStore } from '@/store'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
|
||||
interface DataProps {
|
||||
renderKey: string
|
||||
renderValue: string
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const show = computed({
|
||||
get: () => props.visible,
|
||||
set: (visible: boolean) => emit('update:visible', visible),
|
||||
})
|
||||
|
||||
const showModal = ref(false)
|
||||
|
||||
const importLoading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
|
||||
const searchValue = ref<string>('')
|
||||
|
||||
// 移动端自适应相关
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const promptStore = usePromptStore()
|
||||
|
||||
// Prompt在线导入推荐List,根据部署者喜好进行修改(assets/recommend.json)
|
||||
const promptRecommendList = PromptRecommend
|
||||
const promptList = ref<any>(promptStore.promptList)
|
||||
|
||||
// 用于添加修改的临时prompt参数
|
||||
const tempPromptKey = ref('')
|
||||
const tempPromptValue = ref('')
|
||||
|
||||
// Modal模式,根据不同模式渲染不同的Modal内容
|
||||
const modalMode = ref('')
|
||||
|
||||
// 这个是为了后期的修改Prompt内容考虑,因为要针对无uuid的list进行修改,且考虑到不能出现标题和内容的冲突,所以就需要一个临时item来记录一下
|
||||
const tempModifiedItem = ref<any>({})
|
||||
|
||||
// 添加修改导入都使用一个Modal, 临时修改内容占用tempPromptKey,切换状态前先将内容都清楚
|
||||
const changeShowModal = (mode: 'add' | 'modify' | 'local_import', selected = { key: '', value: '' }) => {
|
||||
if (mode === 'add') {
|
||||
tempPromptKey.value = ''
|
||||
tempPromptValue.value = ''
|
||||
}
|
||||
else if (mode === 'modify') {
|
||||
tempModifiedItem.value = { ...selected }
|
||||
tempPromptKey.value = selected.key
|
||||
tempPromptValue.value = selected.value
|
||||
}
|
||||
else if (mode === 'local_import') {
|
||||
tempPromptKey.value = 'local_import'
|
||||
tempPromptValue.value = ''
|
||||
}
|
||||
showModal.value = !showModal.value
|
||||
modalMode.value = mode
|
||||
}
|
||||
|
||||
// 在线导入相关
|
||||
const downloadURL = ref('')
|
||||
const downloadDisabled = computed(() => downloadURL.value.trim().length < 1)
|
||||
const setDownloadURL = (url: string) => {
|
||||
downloadURL.value = url
|
||||
}
|
||||
|
||||
// 控制 input 按钮
|
||||
const inputStatus = computed (() => tempPromptKey.value.trim().length < 1 || tempPromptValue.value.trim().length < 1)
|
||||
|
||||
// Prompt模板相关操作
|
||||
const addPromptTemplate = () => {
|
||||
for (const i of promptList.value) {
|
||||
if (i.key === tempPromptKey.value) {
|
||||
message.error(t('store.addRepeatTitleTips'))
|
||||
return
|
||||
}
|
||||
if (i.value === tempPromptValue.value) {
|
||||
message.error(t('store.addRepeatContentTips', { msg: tempPromptKey.value }))
|
||||
return
|
||||
}
|
||||
}
|
||||
promptList.value.unshift({ key: tempPromptKey.value, value: tempPromptValue.value } as never)
|
||||
message.success(t('common.addSuccess'))
|
||||
changeShowModal('add')
|
||||
}
|
||||
|
||||
const modifyPromptTemplate = () => {
|
||||
let index = 0
|
||||
|
||||
// 通过临时索引把待修改项摘出来
|
||||
for (const i of promptList.value) {
|
||||
if (i.key === tempModifiedItem.value.key && i.value === tempModifiedItem.value.value)
|
||||
break
|
||||
index = index + 1
|
||||
}
|
||||
|
||||
const tempList = promptList.value.filter((_: any, i: number) => i !== index)
|
||||
|
||||
// 搜索有冲突的部分
|
||||
for (const i of tempList) {
|
||||
if (i.key === tempPromptKey.value) {
|
||||
message.error(t('store.editRepeatTitleTips'))
|
||||
return
|
||||
}
|
||||
if (i.value === tempPromptValue.value) {
|
||||
message.error(t('store.editRepeatContentTips', { msg: i.key }))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
promptList.value = [{ key: tempPromptKey.value, value: tempPromptValue.value }, ...tempList] as never
|
||||
message.success(t('common.editSuccess'))
|
||||
changeShowModal('modify')
|
||||
}
|
||||
|
||||
const deletePromptTemplate = (row: { key: string; value: string }) => {
|
||||
promptList.value = [
|
||||
...promptList.value.filter((item: { key: string; value: string }) => item.key !== row.key),
|
||||
] as never
|
||||
message.success(t('common.deleteSuccess'))
|
||||
}
|
||||
|
||||
const clearPromptTemplate = () => {
|
||||
promptList.value = []
|
||||
message.success(t('common.clearSuccess'))
|
||||
}
|
||||
|
||||
const importPromptTemplate = () => {
|
||||
try {
|
||||
const jsonData = JSON.parse(tempPromptValue.value)
|
||||
let key = ''
|
||||
let value = ''
|
||||
// 可以扩展加入更多模板字典的key
|
||||
if ('key' in jsonData[0]) {
|
||||
key = 'key'
|
||||
value = 'value'
|
||||
}
|
||||
else if ('act' in jsonData[0]) {
|
||||
key = 'act'
|
||||
value = 'prompt'
|
||||
}
|
||||
else {
|
||||
// 不支持的字典的key防止导入 以免破坏prompt商店打开
|
||||
message.warning('prompt key not supported.')
|
||||
throw new Error('prompt key not supported.')
|
||||
}
|
||||
|
||||
for (const i of jsonData) {
|
||||
if (!('key' in i) || !('value' in i))
|
||||
throw new Error(t('store.importError'))
|
||||
let safe = true
|
||||
for (const j of promptList.value) {
|
||||
if (j.key === i[key]) {
|
||||
message.warning(t('store.importRepeatTitle', { msg: i[key] }))
|
||||
safe = false
|
||||
break
|
||||
}
|
||||
if (j.value === i[value]) {
|
||||
message.warning(t('store.importRepeatContent', { msg: i[key] }))
|
||||
safe = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if (safe)
|
||||
promptList.value.unshift({ key: i[key], value: i[value] } as never)
|
||||
}
|
||||
message.success(t('common.importSuccess'))
|
||||
}
|
||||
catch {
|
||||
message.error('JSON 格式错误,请检查 JSON 格式')
|
||||
}
|
||||
}
|
||||
|
||||
// 模板导出
|
||||
const exportPromptTemplate = () => {
|
||||
exportLoading.value = true
|
||||
const jsonDataStr = JSON.stringify(promptList.value)
|
||||
const blob = new Blob([jsonDataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'ChatGPTPromptTemplate.json'
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
exportLoading.value = false
|
||||
}
|
||||
|
||||
// 模板在线导入
|
||||
const downloadPromptTemplate = async () => {
|
||||
try {
|
||||
importLoading.value = true
|
||||
const response = await fetch(downloadURL.value)
|
||||
const jsonData = await response.json()
|
||||
if ('key' in jsonData[0] && 'value' in jsonData[0])
|
||||
tempPromptValue.value = JSON.stringify(jsonData)
|
||||
if ('act' in jsonData[0] && 'prompt' in jsonData[0]) {
|
||||
const newJsonData = jsonData.map((item: { act: string; prompt: string }) => {
|
||||
return {
|
||||
key: item.act,
|
||||
value: item.prompt,
|
||||
}
|
||||
})
|
||||
tempPromptValue.value = JSON.stringify(newJsonData)
|
||||
}
|
||||
importPromptTemplate()
|
||||
downloadURL.value = ''
|
||||
}
|
||||
catch {
|
||||
message.error(t('store.downloadError'))
|
||||
downloadURL.value = ''
|
||||
}
|
||||
finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端自适应相关
|
||||
const renderTemplate = () => {
|
||||
const [keyLimit, valueLimit] = isMobile.value ? [10, 30] : [15, 50]
|
||||
|
||||
return promptList.value.map((item: { key: string; value: string }) => {
|
||||
return {
|
||||
renderKey: item.key.length <= keyLimit ? item.key : `${item.key.substring(0, keyLimit)}...`,
|
||||
renderValue: item.value.length <= valueLimit ? item.value : `${item.value.substring(0, valueLimit)}...`,
|
||||
key: item.key,
|
||||
value: item.value,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const pagination = computed(() => {
|
||||
const [pageSize, pageSlot] = isMobile.value ? [6, 5] : [7, 15]
|
||||
return {
|
||||
pageSize, pageSlot,
|
||||
}
|
||||
})
|
||||
|
||||
// table相关
|
||||
const createColumns = (): DataTableColumns<DataProps> => {
|
||||
return [
|
||||
{
|
||||
title: t('store.title'),
|
||||
key: 'renderKey',
|
||||
},
|
||||
{
|
||||
title: t('store.description'),
|
||||
key: 'renderValue',
|
||||
},
|
||||
{
|
||||
title: t('common.action'),
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render(row) {
|
||||
return h('div', { class: 'flex items-center flex-col gap-2' }, {
|
||||
default: () => [h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => changeShowModal('modify', row),
|
||||
},
|
||||
{ default: () => t('common.edit') },
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
onClick: () => deletePromptTemplate(row),
|
||||
},
|
||||
{ default: () => t('common.delete') },
|
||||
),
|
||||
],
|
||||
})
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const columns = createColumns()
|
||||
|
||||
watch(
|
||||
() => promptList,
|
||||
() => {
|
||||
promptStore.updatePromptList(promptList.value)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const dataSource = computed(() => {
|
||||
const data = renderTemplate()
|
||||
const value = searchValue.value
|
||||
if (value && value !== '') {
|
||||
return data.filter((item: DataProps) => {
|
||||
return item.renderKey.includes(value) || item.renderValue.includes(value)
|
||||
})
|
||||
}
|
||||
return data
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" style="width: 90%; max-width: 900px;" preset="card">
|
||||
<div class="space-y-4">
|
||||
<NTabs type="segment">
|
||||
<NTabPane name="local" :tab="$t('store.local')">
|
||||
<div
|
||||
class="flex gap-3 mb-4"
|
||||
:class="[isMobile ? 'flex-col' : 'flex-row justify-between']"
|
||||
>
|
||||
<div class="flex items-center space-x-4">
|
||||
<NButton
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="changeShowModal('add')"
|
||||
>
|
||||
{{ $t('common.add') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="changeShowModal('local_import')"
|
||||
>
|
||||
{{ $t('common.import') }}
|
||||
</NButton>
|
||||
<NButton
|
||||
size="small"
|
||||
:loading="exportLoading"
|
||||
@click="exportPromptTemplate()"
|
||||
>
|
||||
{{ $t('common.export') }}
|
||||
</NButton>
|
||||
<NPopconfirm @positive-click="clearPromptTemplate">
|
||||
<template #trigger>
|
||||
<NButton size="small">
|
||||
{{ $t('common.clear') }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ $t('store.clearStoreConfirm') }}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<NInput v-model:value="searchValue" style="width: 100%" />
|
||||
</div>
|
||||
</div>
|
||||
<NDataTable
|
||||
v-if="!isMobile"
|
||||
:max-height="400"
|
||||
:columns="columns"
|
||||
:data="dataSource"
|
||||
:pagination="pagination"
|
||||
:bordered="false"
|
||||
/>
|
||||
<NList v-if="isMobile" style="max-height: 400px; overflow-y: auto;">
|
||||
<NListItem v-for="(item, index) of dataSource" :key="index">
|
||||
<NThing :title="item.renderKey" :description="item.renderValue" />
|
||||
<template #suffix>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<NButton tertiary size="small" type="info" @click="changeShowModal('modify', item)">
|
||||
{{ t('common.edit') }}
|
||||
</NButton>
|
||||
<NButton tertiary size="small" type="error" @click="deletePromptTemplate(item)">
|
||||
{{ t('common.delete') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NTabPane>
|
||||
<NTabPane name="download" :tab="$t('store.online')">
|
||||
<p class="mb-4">
|
||||
{{ $t('store.onlineImportWarning') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<NInput v-model:value="downloadURL" placeholder="" />
|
||||
<NButton
|
||||
strong
|
||||
secondary
|
||||
:disabled="downloadDisabled"
|
||||
:loading="importLoading"
|
||||
@click="downloadPromptTemplate()"
|
||||
>
|
||||
{{ $t('common.download') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<NDivider />
|
||||
<div class="max-h-[360px] overflow-y-auto space-y-4">
|
||||
<NCard
|
||||
v-for="info in promptRecommendList"
|
||||
:key="info.key" :title="info.key"
|
||||
:bordered="true"
|
||||
embedded
|
||||
>
|
||||
<p
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
:title="info.desc"
|
||||
>
|
||||
{{ info.desc }}
|
||||
</p>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-end space-x-4">
|
||||
<NButton text>
|
||||
<a
|
||||
:href="info.url"
|
||||
target="_blank"
|
||||
>
|
||||
<SvgIcon class="text-xl" icon="ri:link" />
|
||||
</a>
|
||||
</NButton>
|
||||
<NButton text @click="setDownloadURL(info.downloadUrl) ">
|
||||
<SvgIcon class="text-xl" icon="ri:add-fill" />
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NCard>
|
||||
</div>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</NModal>
|
||||
|
||||
<NModal v-model:show="showModal" style="width: 90%; max-width: 600px;" preset="card">
|
||||
<NSpace v-if="modalMode === 'add' || modalMode === 'modify'" vertical>
|
||||
{{ t('store.title') }}
|
||||
<NInput v-model:value="tempPromptKey" />
|
||||
{{ t('store.description') }}
|
||||
<NInput v-model:value="tempPromptValue" type="textarea" />
|
||||
<NButton
|
||||
block
|
||||
type="primary"
|
||||
:disabled="inputStatus"
|
||||
@click="() => { modalMode === 'add' ? addPromptTemplate() : modifyPromptTemplate() }"
|
||||
>
|
||||
{{ t('common.confirm') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NSpace v-if="modalMode === 'local_import'" vertical>
|
||||
<NInput
|
||||
v-model:value="tempPromptValue"
|
||||
:placeholder="t('store.importPlaceholder')"
|
||||
:autosize="{ minRows: 3, maxRows: 15 }"
|
||||
type="textarea"
|
||||
/>
|
||||
<NButton
|
||||
block
|
||||
type="primary"
|
||||
:disabled="inputStatus"
|
||||
@click="() => { importPromptTemplate() }"
|
||||
>
|
||||
{{ t('common.import') }}
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</NModal>
|
||||
</template>
|
||||
75
ai-chat-web/src/components/common/Setting/About.vue
Normal file
75
ai-chat-web/src/components/common/Setting/About.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { NSpin } from 'naive-ui'
|
||||
import { fetchChatConfig } from '@/api'
|
||||
import pkg from '@/../package.json'
|
||||
import { useAuthStore } from '@/store'
|
||||
|
||||
interface ConfigState {
|
||||
timeoutMs?: number
|
||||
reverseProxy?: string
|
||||
apiModel?: string
|
||||
socksProxy?: string
|
||||
httpsProxy?: string
|
||||
balance?: string
|
||||
}
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const config = ref<ConfigState>()
|
||||
|
||||
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
|
||||
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data } = await fetchChatConfig<ConfigState>()
|
||||
config.value = data
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchConfig()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin :show="loading">
|
||||
<div class="p-4 space-y-4">
|
||||
<h2 class="text-xl font-bold">
|
||||
Version - {{ pkg.version }}
|
||||
</h2>
|
||||
<div class="p-2 space-y-2 rounded-md bg-neutral-100 dark:bg-neutral-700">
|
||||
<p>
|
||||
此项目开源于
|
||||
<a
|
||||
class="text-blue-600 dark:text-blue-500"
|
||||
href="https://github.com/Chanzhaoyu/chatgpt-web"
|
||||
target="_blank"
|
||||
>
|
||||
Github
|
||||
</a>
|
||||
,免费且基于 MIT 协议,没有任何形式的付费行为!
|
||||
</p>
|
||||
<p>
|
||||
如果你觉得此项目对你有帮助,请在 Github 帮我点个 Star 或者给予一点赞助,谢谢!
|
||||
</p>
|
||||
</div>
|
||||
<p>{{ $t("setting.api") }}:{{ config?.apiModel ?? '-' }}</p>
|
||||
<p v-if="isChatGPTAPI">
|
||||
{{ $t("setting.balance") }}:{{ config?.balance ?? '-' }}
|
||||
</p>
|
||||
<p v-if="!isChatGPTAPI">
|
||||
{{ $t("setting.reverseProxy") }}:{{ config?.reverseProxy ?? '-' }}
|
||||
</p>
|
||||
<p>{{ $t("setting.timeout") }}:{{ config?.timeoutMs ?? '-' }}</p>
|
||||
<p>{{ $t("setting.socks") }}:{{ config?.socksProxy ?? '-' }}</p>
|
||||
<p>{{ $t("setting.httpsProxy") }}:{{ config?.httpsProxy ?? '-' }}</p>
|
||||
</div>
|
||||
</NSpin>
|
||||
</template>
|
||||
46
ai-chat-web/src/components/common/Setting/Advanced.vue
Normal file
46
ai-chat-web/src/components/common/Setting/Advanced.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { NButton, NInput, useMessage } from 'naive-ui'
|
||||
import { useSettingStore } from '@/store'
|
||||
import type { SettingsState } from '@/store/modules/settings/helper'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const ms = useMessage()
|
||||
|
||||
const systemMessage = ref(settingStore.systemMessage ?? '')
|
||||
|
||||
function updateSettings(options: Partial<SettingsState>) {
|
||||
settingStore.updateSetting(options)
|
||||
ms.success(t('common.success'))
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
settingStore.resetSetting()
|
||||
ms.success(t('common.success'))
|
||||
window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 space-y-5 min-h-[200px]">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.role') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="systemMessage" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateSettings({ systemMessage })">
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]"> </span>
|
||||
<NButton size="small" @click="handleReset">
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
223
ai-chat-web/src/components/common/Setting/General.vue
Normal file
223
ai-chat-web/src/components/common/Setting/General.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { NButton, NInput, NPopconfirm, NSelect, useMessage } from 'naive-ui'
|
||||
import type { Language, Theme } from '@/store/modules/app/helper'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useAppStore, useUserStore } from '@/store'
|
||||
import type { UserInfo } from '@/store/modules/user/helper'
|
||||
import { getCurrentDate } from '@/utils/functions'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const ms = useMessage()
|
||||
|
||||
const theme = computed(() => appStore.theme)
|
||||
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
|
||||
const avatar = ref(userInfo.value.avatar ?? '')
|
||||
|
||||
const name = ref(userInfo.value.name ?? '')
|
||||
|
||||
const description = ref(userInfo.value.description ?? '')
|
||||
|
||||
const language = computed({
|
||||
get() {
|
||||
return appStore.language
|
||||
},
|
||||
set(value: Language) {
|
||||
appStore.setLanguage(value)
|
||||
},
|
||||
})
|
||||
|
||||
const themeOptions: { label: string; key: Theme; icon: string }[] = [
|
||||
{
|
||||
label: 'Auto',
|
||||
key: 'auto',
|
||||
icon: 'ri:contrast-line',
|
||||
},
|
||||
{
|
||||
label: 'Light',
|
||||
key: 'light',
|
||||
icon: 'ri:sun-foggy-line',
|
||||
},
|
||||
{
|
||||
label: 'Dark',
|
||||
key: 'dark',
|
||||
icon: 'ri:moon-foggy-line',
|
||||
},
|
||||
]
|
||||
|
||||
const languageOptions: { label: string; key: Language; value: Language }[] = [
|
||||
{ label: '简体中文', key: 'zh-CN', value: 'zh-CN' },
|
||||
{ label: '繁體中文', key: 'zh-TW', value: 'zh-TW' },
|
||||
{ label: 'English', key: 'en-US', value: 'en-US' },
|
||||
]
|
||||
|
||||
function updateUserInfo(options: Partial<UserInfo>) {
|
||||
userStore.updateUserInfo(options)
|
||||
ms.success(t('common.success'))
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
userStore.resetUserInfo()
|
||||
ms.success(t('common.success'))
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
function exportData(): void {
|
||||
const date = getCurrentDate()
|
||||
const data: string = localStorage.getItem('chatStorage') || '{}'
|
||||
const jsonString: string = JSON.stringify(JSON.parse(data), null, 2)
|
||||
const blob: Blob = new Blob([jsonString], { type: 'application/json' })
|
||||
const url: string = URL.createObjectURL(blob)
|
||||
const link: HTMLAnchorElement = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `chat-store_${date}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
function importData(event: Event): void {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (!target || !target.files)
|
||||
return
|
||||
|
||||
const file: File = target.files[0]
|
||||
if (!file)
|
||||
return
|
||||
|
||||
const reader: FileReader = new FileReader()
|
||||
reader.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(reader.result as string)
|
||||
localStorage.setItem('chatStorage', JSON.stringify(data))
|
||||
ms.success(t('common.success'))
|
||||
location.reload()
|
||||
}
|
||||
catch (error) {
|
||||
ms.error(t('common.invalidFileFormat'))
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
function clearData(): void {
|
||||
localStorage.removeItem('chatStorage')
|
||||
location.reload()
|
||||
}
|
||||
|
||||
function handleImportButtonClick(): void {
|
||||
const fileInput = document.getElementById('fileInput') as HTMLElement
|
||||
if (fileInput)
|
||||
fileInput.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4 space-y-5 min-h-[200px]">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.avatarLink') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="avatar" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateUserInfo({ avatar })">
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.name') }}</span>
|
||||
<div class="w-[200px]">
|
||||
<NInput v-model:value="name" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateUserInfo({ name })">
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.description') }}</span>
|
||||
<div class="flex-1">
|
||||
<NInput v-model:value="description" placeholder="" />
|
||||
</div>
|
||||
<NButton size="tiny" text type="primary" @click="updateUserInfo({ description })">
|
||||
{{ $t('common.save') }}
|
||||
</NButton>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center space-x-4"
|
||||
:class="isMobile && 'items-start'"
|
||||
>
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatHistory') }}</span>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<NButton size="small" @click="exportData">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:download-2-fill" />
|
||||
</template>
|
||||
{{ $t('common.export') }}
|
||||
</NButton>
|
||||
|
||||
<input id="fileInput" type="file" style="display:none" @change="importData">
|
||||
<NButton size="small" @click="handleImportButtonClick">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:upload-2-fill" />
|
||||
</template>
|
||||
{{ $t('common.import') }}
|
||||
</NButton>
|
||||
|
||||
<NPopconfirm placement="bottom" @positive-click="clearData">
|
||||
<template #trigger>
|
||||
<NButton size="small">
|
||||
<template #icon>
|
||||
<SvgIcon icon="ri:close-circle-line" />
|
||||
</template>
|
||||
{{ $t('common.clear') }}
|
||||
</NButton>
|
||||
</template>
|
||||
{{ $t('chat.clearHistoryConfirm') }}
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.theme') }}</span>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<template v-for="item of themeOptions" :key="item.key">
|
||||
<NButton
|
||||
size="small"
|
||||
:type="item.key === theme ? 'primary' : undefined"
|
||||
@click="appStore.setTheme(item.key)"
|
||||
>
|
||||
<template #icon>
|
||||
<SvgIcon :icon="item.icon" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.language') }}</span>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<NSelect
|
||||
style="width: 140px"
|
||||
:value="language"
|
||||
:options="languageOptions"
|
||||
@update-value="value => appStore.setLanguage(value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="flex-shrink-0 w-[100px]">{{ $t('setting.resetUserInfo') }}</span>
|
||||
<NButton size="small" @click="handleReset">
|
||||
{{ $t('common.reset') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
70
ai-chat-web/src/components/common/Setting/index.vue
Normal file
70
ai-chat-web/src/components/common/Setting/index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, ref } from 'vue'
|
||||
import { NModal, NTabPane, NTabs } from 'naive-ui'
|
||||
import General from './General.vue'
|
||||
import Advanced from './Advanced.vue'
|
||||
import About from './About.vue'
|
||||
import { useAuthStore } from '@/store'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
|
||||
interface Props {
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
interface Emit {
|
||||
(e: 'update:visible', visible: boolean): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<Emit>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const isChatGPTAPI = computed<boolean>(() => !!authStore.isChatGPTAPI)
|
||||
|
||||
const active = ref('General')
|
||||
|
||||
const show = computed({
|
||||
get() {
|
||||
return props.visible
|
||||
},
|
||||
set(visible: boolean) {
|
||||
emit('update:visible', visible)
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal v-model:show="show" :auto-focus="false" preset="card" style="width: 95%; max-width: 640px">
|
||||
<div>
|
||||
<NTabs v-model:value="active" type="line" animated>
|
||||
<NTabPane name="General" tab="General">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:file-user-line" />
|
||||
<span class="ml-2">{{ $t('setting.general') }}</span>
|
||||
</template>
|
||||
<div class="min-h-[100px]">
|
||||
<General />
|
||||
</div>
|
||||
</NTabPane>
|
||||
<NTabPane v-if="isChatGPTAPI" name="Advanced" tab="Advanced">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:equalizer-line" />
|
||||
<span class="ml-2">{{ $t('setting.advanced') }}</span>
|
||||
</template>
|
||||
<div class="min-h-[100px]">
|
||||
<Advanced />
|
||||
</div>
|
||||
</NTabPane>
|
||||
<NTabPane name="Config" tab="Config">
|
||||
<template #tab>
|
||||
<SvgIcon class="text-lg" icon="ri:list-settings-line" />
|
||||
<span class="ml-2">{{ $t('setting.config') }}</span>
|
||||
</template>
|
||||
<About />
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</div>
|
||||
</NModal>
|
||||
</template>
|
||||
21
ai-chat-web/src/components/common/SvgIcon/index.vue
Normal file
21
ai-chat-web/src/components/common/SvgIcon/index.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed, useAttrs } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
interface Props {
|
||||
icon?: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const attrs = useAttrs()
|
||||
|
||||
const bindAttrs = computed<{ class: string; style: string }>(() => ({
|
||||
class: (attrs.class as string) || '',
|
||||
style: (attrs.style as string) || '',
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Icon :icon="icon" v-bind="bindAttrs" />
|
||||
</template>
|
||||
40
ai-chat-web/src/components/common/UserAvatar/index.vue
Normal file
40
ai-chat-web/src/components/common/UserAvatar/index.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed } from 'vue'
|
||||
import { NAvatar } from 'naive-ui'
|
||||
import { useUserStore } from '@/store'
|
||||
import defaultAvatar from '@/assets/avatar.jpg'
|
||||
import { isString } from '@/utils/is'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const userInfo = computed(() => userStore.userInfo)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center overflow-hidden">
|
||||
<div class="w-10 h-10 overflow-hidden rounded-full shrink-0">
|
||||
<template v-if="isString(userInfo.avatar) && userInfo.avatar.length > 0">
|
||||
<NAvatar
|
||||
size="large"
|
||||
round
|
||||
:src="userInfo.avatar"
|
||||
:fallback-src="defaultAvatar"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<NAvatar size="large" round :src="defaultAvatar" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 ml-2">
|
||||
<h2 class="overflow-hidden font-bold text-md text-ellipsis whitespace-nowrap">
|
||||
{{ userInfo.name ?? 'ChenZhaoYu' }}
|
||||
</h2>
|
||||
<p class="overflow-hidden text-xs text-gray-500 text-ellipsis whitespace-nowrap">
|
||||
<span
|
||||
v-if="isString(userInfo.description) && userInfo.description !== ''"
|
||||
v-html="userInfo.description"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
8
ai-chat-web/src/components/common/index.ts
Normal file
8
ai-chat-web/src/components/common/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import HoverButton from './HoverButton/index.vue'
|
||||
import NaiveProvider from './NaiveProvider/index.vue'
|
||||
import SvgIcon from './SvgIcon/index.vue'
|
||||
import UserAvatar from './UserAvatar/index.vue'
|
||||
import Setting from './Setting/index.vue'
|
||||
import PromptStore from './PromptStore/index.vue'
|
||||
|
||||
export { HoverButton, NaiveProvider, SvgIcon, UserAvatar, Setting, PromptStore }
|
||||
Reference in New Issue
Block a user