提交 d5f82065 编写于 作者: D DebugIsFalse

fix: eslint

上级 ddccb0a3
......@@ -22,4 +22,6 @@ logs
.env
.env.*
!.env.example
yarn.lock
\ No newline at end of file
yarn.lock
.eslintcache
.vscode/
\ No newline at end of file
<template>
<NuxtPwaManifest />
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UNotifications />
<UModals />
<NuxtPwaManifest />
<NuxtLoadingIndicator />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UNotifications />
<UModals />
</template>
<script setup>
const { $updateUserInfo } = useUserStore()
useHead({
title: 'GitBot AI'
title: 'GitBot AI'
})
nextTick(() => {
$updateUserInfo()
$updateUserInfo()
})
</script>
<template>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<ICollapse :open="openCollapse" class="mt-2">
<div class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400">
<div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-inbox-arrow-down" />
理解问题
</div>
<template v-if="item.desLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<IMdMdc v-else :content="item.description" />
<template v-if="item.searchLoading !== undefined">
<div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-magnifying-glass" />
搜索项目
</div>
<template v-if="item.searchLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<div class="text-xs pl-5" v-else>找到 {{ item.source && item.source.length || 0 }} 条来源</div>
</template>
<div class="text-base flex items-center gap-1" v-if="item.ansLoading !== undefined">
<UIcon name="i-heroicons-pencil-square" />
整理答案
</div>
</div>
</ICollapse>
</UCard>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<ICollapse :open="openCollapse" class="mt-2">
<div class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400">
<div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-inbox-arrow-down" />
理解问题
</div>
<template v-if="item.desLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<IMdMdc v-else :content="item.description" />
<template v-if="item.searchLoading !== undefined">
<div class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-magnifying-glass" />
搜索项目
</div>
<template v-if="item.searchLoading">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<div v-else class="text-xs pl-5">找到 {{ item.source && item.source.length || 0 }} 条来源</div>
</template>
<div v-if="item.ansLoading !== undefined" class="text-base flex items-center gap-1">
<UIcon name="i-heroicons-pencil-square" />
整理答案
</div>
</div>
</ICollapse>
</UCard>
</template>
<script setup>
const props = defineProps({
type: {
type: String,
default: 'search'
},
item: {
type: Object,
default: (() => {})
},
collapse: {
type: Boolean,
default: true
}
type: {
type: String,
default: 'search'
},
item: {
type: Object,
default: (() => {})
},
collapse: {
type: Boolean,
default: true
}
})
const openCollapse = ref(props.collapse)
function handleToggleCollapse () {
openCollapse.value = !openCollapse.value
openCollapse.value = !openCollapse.value
}
function handleCollapse (state) {
openCollapse.value = state
openCollapse.value = state
}
watch(() => props.collapse, () => {
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 500)
}
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 500)
}
}, { immediate: true})
defineExpose({ handleCollapse })
</script>
<template>
<div class="flex flex-col overflow-hidden">
<div class="flex justify-between">
<UButton
class="flex-grow"
leading-icon="i-heroicons-magnifying-glass"
color="gray"
variant="ghost"
size="md"
label="搜索记录"
@click="handleOpen"
/>
<UButton
v-if="$isSignIn"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</div>
<div v-if="$isSignIn" class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
size="xs"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</div>
<UModal v-model="isOpenHistory" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<div class="flex items-center p-2">
<UInput
class="w-full"
v-model="query"
:padded="false"
variant="none"
leading-icon="i-heroicons-magnifying-glass-20-solid"
placeholder="输入关键字搜索..."
/>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
<UDivider />
<div class="flex flex-col p-2">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group"
color="gray"
variant="ghost"
long
leading-icon="i-heroicons-document-text"
:to="`/search/${item.c_id}`"
@click="handleClose"
>
<div class="flex-grow truncate font-light">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</UModal>
<div class="flex flex-col overflow-hidden">
<div class="flex justify-between">
<UButton
class="flex-grow"
leading-icon="i-heroicons-magnifying-glass"
color="gray"
variant="ghost"
size="md"
label="搜索记录"
@click="handleOpen"
/>
<UButton
v-if="$isSignIn"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</div>
<div v-if="$isSignIn" class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
size="xs"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</div>
<UModal v-model="isOpenHistory" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<div class="flex items-center p-2">
<UInput
v-model="query"
class="w-full"
:padded="false"
variant="none"
leading-icon="i-heroicons-magnifying-glass-20-solid"
placeholder="输入关键字搜索..."
/>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
<UDivider />
<div class="flex flex-col p-2">
<template v-for="(item, index) in $searchHistory" :key="index">
<UButton
class="flex group"
color="gray"
variant="ghost"
long
leading-icon="i-heroicons-document-text"
:to="`/search/${item.c_id}`"
@click="handleClose"
>
<div class="flex-grow truncate font-light">{{ item.title }}</div>
<UButton
class="hidden group-hover:flex"
color="red"
variant="ghost"
:padded="false"
leading-icon="i-heroicons-x-mark-20-solid"
@click.stop.prevent="handleRemoveRecordItem(item.c_id)"
/>
</UButton>
</template>
</div>
</UModal>
</template>
<script setup>
import { IConfirm } from '#components'
......@@ -95,45 +95,45 @@ const { $getSearchHistory } = useSearchStore()
const isOpenHistory = ref(false)
const query = ref('')
function handleClear () {
modal.open(IConfirm, {
title: '清空确认',
description: '确定要清空全部搜索记录吗?',
onSuccess () {
modal.close()
emits('clear')
handleRemoveRecords()
},
onCancel () {
modal.close()
}
})
modal.open(IConfirm, {
title: '清空确认',
description: '确定要清空全部搜索记录吗?',
onSuccess () {
modal.close()
emits('clear')
handleRemoveRecords()
},
onCancel () {
modal.close()
}
})
}
function handleRemoveRecordItem (id) {
handleRemoveRecords([id])
handleRemoveRecords([id])
}
async function handleRemoveRecords (ids) {
if (!ids) {
ids = $searchHistory.value.map(item => item.c_id)
}
const { data} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
if (data.value) {
$getSearchHistory()
navigateTo('/')
}
if (!ids) {
ids = $searchHistory.value.map(item => item.c_id)
}
const { data} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
if (data.value) {
$getSearchHistory()
navigateTo('/')
}
}
function handleOpen () {
if (!$isSignIn) emits('sign')
else {
isOpenHistory.value = true
}
if (!$isSignIn) emits('sign')
else {
isOpenHistory.value = true
}
}
function handleClose () {
isOpenHistory.value = false
isOpenHistory.value = false
}
nextTick(() => {
$getSearchHistory()
$getSearchHistory()
})
</script>
<template>
<UPopover :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
icon="i-heroicons-swatch-16-solid"
:ui="{ icon: { base: 'text-primary-500 dark:text-primary-400' } }"
aria-label="Color picker"
/>
</template>
<template #panel>
<div class="flex flex-col p-2 gap-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<UDivider />
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
</UPopover>
<UPopover :popper="{ strategy: 'absolute' }" :ui="{ width: 'w-[156px]' }">
<template #default="{ open }">
<UButton
color="gray"
variant="ghost"
square
:class="[open && 'bg-gray-50 dark:bg-gray-800']"
icon="i-heroicons-swatch-16-solid"
:ui="{ icon: { base: 'text-primary-500 dark:text-primary-400' } }"
aria-label="Color picker"
/>
</template>
<template #panel>
<div class="flex flex-col p-2 gap-2">
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in primaryColors" :key="color.value" :color="color" :selected="primary" @select="primary = color" />
</div>
<UDivider />
<div class="grid grid-cols-5 gap-px">
<ColorPickerPill v-for="color in grayColors" :key="color.value" :color="color" :selected="gray" @select="gray = color" />
</div>
</div>
</template>
</UPopover>
</template>
<script setup lang="ts">
......@@ -38,25 +38,25 @@ const colorMode = useColorMode()
const primaryColors = computed(() => appConfig.ui.colors.filter(color => color !== 'primary').map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const primary = computed({
get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set (option) {
appConfig.ui.primary = option.value
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
}
get () {
return primaryColors.value.find(option => option.value === appConfig.ui.primary)
},
set (option) {
appConfig.ui.primary = option.value
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.primary)
}
})
const grayColors = computed(() => ['slate', 'cool', 'zinc', 'neutral', 'stone'].map(color => ({ value: color, text: color, hex: colors[color][colorMode.value === 'dark' ? 400 : 500] })))
const gray = computed({
get () {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set (option) {
appConfig.ui.gray = option.value
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
}
get () {
return grayColors.value.find(option => option.value === appConfig.ui.gray)
},
set (option) {
appConfig.ui.gray = option.value
window.localStorage.setItem('nuxt-ui-gray', appConfig.ui.gray)
}
})
</script>
\ No newline at end of file
<template>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="white"
square
:ui="{
color: {
white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}
}
}"
:variant="color.value === selected.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton>
</UTooltip>
<UTooltip :text="color.value" class="capitalize" :open-delay="500">
<UButton
color="white"
square
:ui="{
color: {
white: {
solid: 'ring-0 bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800',
ghost: 'hover:bg-gray-50 dark:hover:bg-gray-800/50'
}
}
}"
:variant="color.value === selected.value ? 'solid' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span class="inline-block w-3 h-3 rounded-full" :style="{ backgroundColor: color.hex }" />
</UButton>
</UTooltip>
</template>
<script setup lang="ts">
......
<template>
<div class="flex-grow overflow-hidden bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800">
<div class="p-4 h-full flex flex-col">
<ILogo class="mt-2" />
<UButton
:ui="{ rounded: 'rounded-full' }"
class="flex w-full mt-6"
color="gray"
leading-icon="i-heroicons-plus-20-solid"
size="md"
@click="handleShowCreate"
>
<div class="flex flex-grow justify-between items-center">
<span>新主题</span>
<div class="flex items-center gap-0.5" v-if="$device.isDesktop">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</div>
</UButton>
<div class="flex flex-grow overflow-hidden mt-4">
<INav />
</div>
</div>
</div>
<UDivider />
<div class="bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col items-center justify-center">
<IUserInfo />
</div>
<UModal v-model="isOpenCreate" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<ICreate @search="handleCloseCreate" />
</UModal>
<div class="flex-grow overflow-hidden bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800">
<div class="p-4 h-full flex flex-col">
<ILogo class="mt-2" />
<UButton
:ui="{ rounded: 'rounded-full' }"
class="flex w-full mt-6"
color="gray"
leading-icon="i-heroicons-plus-20-solid"
size="md"
@click="handleShowCreate"
>
<div class="flex flex-grow justify-between items-center">
<span>新主题</span>
<div v-if="$device.isDesktop" class="flex items-center gap-0.5">
<UKbd>{{ metaSymbol }}</UKbd>
<UKbd>K</UKbd>
</div>
</div>
</UButton>
<div class="flex flex-grow overflow-hidden mt-4">
<INav />
</div>
</div>
</div>
<UDivider />
<div class="bg-gray-100 dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col items-center justify-center">
<IUserInfo />
</div>
<UModal v-model="isOpenCreate" :ui="{ width: 'w-full sm:max-w-screen-md' }">
<ICreate @search="handleCloseCreate" />
</UModal>
</template>
<script setup>
const { metaSymbol } = useShortcuts()
const isOpenCreate = ref(false)
const handleShowCreate = () => {
isOpenCreate.value = true
isOpenCreate.value = true
}
const handleCloseCreate = () => {
isOpenCreate.value = false
isOpenCreate.value = false
}
defineShortcuts({
meta_k: {
handler: () => {
handleShowCreate()
}
meta_k: {
handler: () => {
handleShowCreate()
}
}
})
</script>
<template>
<Transition
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
>
<div v-show="open" class="flex transition-[height] overflow-hidden"><slot /></div>
</Transition>
<Transition
@enter="onEnter"
@after-enter="onAfterEnter"
@before-leave="onBeforeLeave"
@leave="onLeave"
>
<div v-show="open" class="flex transition-[height] overflow-hidden"><slot /></div>
</Transition>
</template>
<script setup>
const props = defineProps({
open: {
type: Boolean,
default: true
}
defineProps({
open: {
type: Boolean,
default: true
}
})
function onEnter(_el, done) {
const el = _el
el.style.height = '0'
el.offsetHeight
el.style.height = el.scrollHeight + 'px'
el.addEventListener('transitionend', done, { once: true })
const el = _el
el.style.height = '0'
el.offsetHeight
el.style.height = el.scrollHeight + 'px'
el.addEventListener('transitionend', done, { once: true })
}
function onBeforeLeave(_el) {
const el = _el
el.style.height = el.scrollHeight + 'px'
el.offsetHeight
const el = _el
el.style.height = el.scrollHeight + 'px'
el.offsetHeight
}
function onAfterEnter(_el) {
const el = _el
el.style.height = 'auto'
const el = _el
el.style.height = 'auto'
}
function onLeave(_el, done) {
const el = _el
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })
const el = _el
el.style.height = '0'
el.addEventListener('transitionend', done, { once: true })
}
</script>
<template>
<UModal :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<div class="flex p-6 gap-4">
<div class="flex">
<div class="mx-auto flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-red-100">
<UIcon name="i-heroicons-exclamation-triangle" class="w-6 h-6 text-red-600" />
</div>
</div>
<div class="flex-grow flex flex-col">
<div class="text-base leading-6 text-gray-900 dark:text-gray-300 truncate">{{ title }}</div>
<div class="mt-2 text-sm text-gray-500">{{ description }}</div>
</div>
</div>
<div class="p-4 flex justify-end gap-2">
<UButton color="white" @click="handleCancel">取消</UButton>
<UButton color="red" @click="handleSuccess">确定</UButton>
</div>
</UModal>
<UModal :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<div class="flex p-6 gap-4">
<div class="flex">
<div class="mx-auto flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-red-100">
<UIcon name="i-heroicons-exclamation-triangle" class="w-6 h-6 text-red-600" />
</div>
</div>
<div class="flex-grow flex flex-col">
<div class="text-base leading-6 text-gray-900 dark:text-gray-300 truncate">{{ title }}</div>
<div class="mt-2 text-sm text-gray-500">{{ description }}</div>
</div>
</div>
<div class="p-4 flex justify-end gap-2">
<UButton color="white" @click="handleCancel">取消</UButton>
<UButton color="red" @click="handleSuccess">确定</UButton>
</div>
</UModal>
</template>
<script setup>
defineProps({
title: {
type: String,
default: ''
},
description: {
type: String,
default: '确认要全部清空吗?'
},
async: {
type: Boolean,
default: false
}
title: {
type: String,
default: ''
},
description: {
type: String,
default: '确认要全部清空吗?'
},
async: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['success', 'cancel'])
function handleCancel () {
emits('cancel')
emits('cancel')
}
function handleSuccess () {
emits('success')
emits('success')
}
</script>
<template>
<div class="max-w-screen-md w-full flex flex-col space-y-4 p-6">
<UCard
class="transition-[box-shadow] hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI"
>
<UTextarea
name="createInput"
v-model="query"
autoresize
placeholder="输入搜索内容..."
:rows="5"
variant="none"
:padded="false"
maxlength="2000"
/>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<UTooltip class="relative" :text="isPro ? '已开启专家搜索' : '已关闭专家搜索'" :shortcuts="[metaSymbol, 'O']">
<div class="absolute w-7 h-0.5 rotate-45 top-3.5 left-1 bg-gray-700 dark:bg-gray-200 hover:bg-gray-900 dark:hover:bg-white rounded" v-show="!isPro"></div>
<UButton
:ui="{ rounded: 'rounded-full' }"
:icon="isPro ? 'i-heroicons-sparkles-20-solid' : 'i-heroicons-sparkles-20-solid'"
color="gray"
variant="ghost"
@click="handleTogglePro"
/>
</UTooltip>
<USelectMenu
:ui-menu="menuUI"
v-model="selectedRepo"
:options="$repos"
placeholder="选择 GitHub 项目"
value-attribute="label"
option-attribute="label"
>
<UTooltip :text="isPro ? '选择 GitHub 项目' : '选择 GitHub 项目(需开启专家搜索)'" v-if="!selectedRepo">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-simple-icons-github"
color="gray"
variant="ghost"
:disabled="!isPro"
/>
</UTooltip>
<div class="max-w-screen-md w-full flex flex-col space-y-4 p-6">
<UCard
class="transition-[box-shadow] hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI"
>
<UTextarea
v-model="query"
name="createInput"
autoresize
placeholder="输入搜索内容..."
:rows="5"
variant="none"
:padded="false"
maxlength="2000"
/>
<div class="flex justify-between items-center">
<div class="flex items-center gap-2">
<UTooltip class="relative" :text="isPro ? '已开启专家搜索' : '已关闭专家搜索'" :shortcuts="[metaSymbol, 'O']">
<div v-show="!isPro" class="absolute w-7 h-0.5 rotate-45 top-3.5 left-1 bg-gray-700 dark:bg-gray-200 hover:bg-gray-900 dark:hover:bg-white rounded"/>
<UButton
:ui="{ rounded: 'rounded-full' }"
:icon="isPro ? 'i-heroicons-sparkles-20-solid' : 'i-heroicons-sparkles-20-solid'"
color="gray"
variant="ghost"
@click="handleTogglePro"
/>
</UTooltip>
<USelectMenu
v-model="selectedRepo"
:ui-menu="menuUI"
:options="$repos"
placeholder="选择 GitHub 项目"
value-attribute="label"
option-attribute="label"
>
<UTooltip v-if="!selectedRepo" :text="isPro ? '选择 GitHub 项目' : '选择 GitHub 项目(需开启专家搜索)'">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-simple-icons-github"
color="gray"
variant="ghost"
:disabled="!isPro"
/>
</UTooltip>
<UButton v-else color="gray" variant="ghost" :class="{ 'group': selectedRepo }">
<UIcon name="i-simple-icons-github" />
<span>{{ selectedRepo }}</span>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-xl flex group-hover:hidden" />
<UButton
v-if="selectedRepo"
class="hidden group-hover:flex"
@click.stop.prevent="handleClearRepo"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
color="gray"
variant="link"
/>
</UButton>
</USelectMenu>
</div>
<UTooltip text="搜索" :shortcuts="[metaSymbol, '↵']">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-heroicons-chevron-right-20-solid"
@click="handleSearch"
:loading="loading"
:disabled="query === ''"
size="md"
/>
</UTooltip>
</div>
</UCard>
</div>
<UButton v-else color="gray" variant="ghost" :class="{ 'group': selectedRepo }">
<UIcon name="i-simple-icons-github" />
<span>{{ selectedRepo }}</span>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-xl flex group-hover:hidden" />
<UButton
v-if="selectedRepo"
class="hidden group-hover:flex"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
color="gray"
variant="link"
@click.stop.prevent="handleClearRepo"
/>
</UButton>
</USelectMenu>
</div>
<UTooltip text="搜索" :shortcuts="[metaSymbol, '↵']">
<UButton
:ui="{ rounded: 'rounded-full' }"
icon="i-heroicons-chevron-right-20-solid"
:loading="loading"
:disabled="query === ''"
size="md"
@click="handleSearch"
/>
</UTooltip>
</div>
</UCard>
</div>
</template>
<script setup>
const { $repos } = storeToRefs(useReposStore())
......@@ -84,74 +84,74 @@ const query = ref('')
const selectedRepo = ref('')
const loading = ref(false)
const cardUI = {
body: {
padding: 'p-3 sm:p-3'
},
rounded: 'rounded-xl'
body: {
padding: 'p-3 sm:p-3'
},
rounded: 'rounded-xl'
}
const menuUI = {
width: 'w-auto'
width: 'w-auto'
}
const isPro = ref(true)
const handleSearch = async () => {
if (loading.value || query.value === '') return
loading.value = true
const currentRepo = $repos.value.find(item => item.label === selectedRepo.value)
const repo_path = currentRepo ? currentRepo.url : ''
const body = {
title: query.value,
repo_path: repo_path || ''
}
const { data, error } = await useRequest('/v1/chat/completion/create', { method: 'post', body })
loading.value = false
if (error.value) return
// todo 临时存到pina给搜索使用
$setFirstRecordTitle(query.value)
navigateTo(`/search/${data.value.data.c_id}`)
emits('search')
if (loading.value || query.value === '') return
loading.value = true
const currentRepo = $repos.value.find(item => item.label === selectedRepo.value)
const repo_path = currentRepo ? currentRepo.url : ''
const body = {
title: query.value,
repo_path: repo_path || ''
}
const { data, error } = await useRequest('/v1/chat/completion/create', { method: 'post', body })
loading.value = false
if (error.value) return
// todo 临时存到pina给搜索使用
$setFirstRecordTitle(query.value)
navigateTo(`/search/${data.value.data.c_id}`)
emits('search')
}
function handleQuickSearch (title) {
query.value = title
handleSearch()
query.value = title
handleSearch()
}
defineExpose({
handleQuickSearch
handleQuickSearch
})
function handleClearRepo () {
selectedRepo.value = ''
selectedRepo.value = ''
}
nextTick(async () => {
if (!$repos.value.length) {
const { data } = await useRequest('/v1/chat/repository')
const repoData = data.value.data.map(item => {
return {
label: item.name,
url: item.path,
branch: item.branch
}
})
$setRepo(repoData)
}
if (!$repos.value.length) {
const { data } = await useRequest('/v1/chat/repository')
const repoData = data.value.data.map(item => {
return {
label: item.name,
url: item.path,
branch: item.branch
}
})
$setRepo(repoData)
}
})
defineShortcuts({
meta_enter: {
usingInput: 'createInput',
handler: () => {
handleSearch()
}
},
meta_o: {
usingInput: 'createInput',
handler: () => {
handleTogglePro()
}
meta_enter: {
usingInput: 'createInput',
handler: () => {
handleSearch()
}
},
meta_o: {
usingInput: 'createInput',
handler: () => {
handleTogglePro()
}
}
})
function handleTogglePro () {
isPro.value = !isPro.value
if (!isPro.value) handleClearRepo()
isPro.value = !isPro.value
if (!isPro.value) handleClearRepo()
}
</script>
<template>
<div class="flex flex-col justify-center h-72 items-center text-gray-300" :class="{ 'h-36': size === 'xs' }">
<UIcon name="i-heroicons-inbox" class="text-6xl" />
<div>暂无数据</div>
</div>
<div class="flex flex-col justify-center h-72 items-center text-gray-300" :class="{ 'h-36': size === 'xs' }">
<UIcon name="i-heroicons-inbox" class="text-6xl" />
<div>暂无数据</div>
</div>
</template>
<script setup>
defineProps({
size: {
type: String,
default: 'sm'
}
size: {
type: String,
default: 'sm'
}
})
</script>
\ No newline at end of file
<template>
<div class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center">
<p class="text-base font-semibold text-red-600">{{ code }}</p>
<h1 class="mt-4 text-3xl font-bold tracking-tight sm:text-5xl">{{ errorTitle }}</h1>
<p class="mt-6 text-base leading-7">{{ errorDescription }}</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<UButton to="/" size="lg">返回首页</UButton>
</div>
</div>
</div>
<div class="grid min-h-full place-items-center px-6 py-24 sm:py-32 lg:px-8">
<div class="text-center">
<p class="text-base font-semibold text-red-600">{{ code }}</p>
<h1 class="mt-4 text-3xl font-bold tracking-tight sm:text-5xl">{{ errorTitle }}</h1>
<p class="mt-6 text-base leading-7">{{ errorDescription }}</p>
<div class="mt-10 flex items-center justify-center gap-x-6">
<UButton to="/" size="lg">返回首页</UButton>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
code: {
type: Number,
default: 404
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
code: {
type: Number,
default: 404
},
title: {
type: String,
default: ''
},
description: {
type: String,
default: ''
}
})
const errorTitle = computed(() => {
let title = props.title
if (!title) {
const code = props.code
if (code === 404) title = 'Not found'
else if (code === 500 || code === 502) title = 'Error'
else if (code === 403) title= 'Forbidden'
}
return title
let title = props.title
if (!title) {
const code = props.code
if (code === 404) title = 'Not found'
else if (code === 500 || code === 502) title = 'Error'
else if (code === 403) title= 'Forbidden'
}
return title
})
const errorDescription = computed(() => {
let description = props.description
if (!description) {
const code = props.code
if (code === 404) description = '抱歉,当前访问的内容不存在或被删除'
else if (code === 500 || code === 502) description = '抱歉,当前内容出现错误'
else if (code === 403) description= '抱歉,您没有权限访问当前内容'
}
return description
let description = props.description
if (!description) {
const code = props.code
if (code === 404) description = '抱歉,当前访问的内容不存在或被删除'
else if (code === 500 || code === 502) description = '抱歉,当前内容出现错误'
else if (code === 403) description= '抱歉,您没有权限访问当前内容'
}
return description
})
</script>
\ No newline at end of file
<template>
<div class="flex justify-center">
<NuxtLink to="/" class="font-mono text-2xl font-medium">GitBot.<span class="text-primary">AI</span></NuxtLink>
</div>
<div class="flex justify-center">
<NuxtLink to="/" class="font-mono text-2xl font-medium">GitBot.<span class="text-primary">AI</span></NuxtLink>
</div>
</template>
\ No newline at end of file
<template>
<div class="flex md:hidden flex-shrink-0">
<UButton
class="z-20"
:class="{ 'fixed top-2 left-2' : fixed }"
color="white"
trailing-icon="i-heroicons-bars-3-20-solid"
@click="handleToggleAside"
/>
<USlideover
class="w-64"
v-model="isOpenAside"
side="left"
:overlay="false"
>
<UButton
class="absolute top-2 right-2 z-20"
color="white"
trailing-icon="i-heroicons-x-mark-20-solid"
@click="handleToggleAside"
/>
<IAside />
</USlideover>
</div>
<div class="flex md:hidden flex-shrink-0">
<UButton
class="z-20"
:class="{ 'fixed top-2 left-2' : fixed }"
color="white"
trailing-icon="i-heroicons-bars-3-20-solid"
@click="handleToggleAside"
/>
<USlideover
v-model="isOpenAside"
class="w-64"
side="left"
:overlay="false"
>
<UButton
class="absolute top-2 right-2 z-20"
color="white"
trailing-icon="i-heroicons-x-mark-20-solid"
@click="handleToggleAside"
/>
<IAside />
</USlideover>
</div>
</template>
<script setup>
defineProps({
fixed: {
type: Boolean,
default: false
}
fixed: {
type: Boolean,
default: false
}
})
const isOpenAside = ref(false)
function handleToggleAside () {
isOpenAside.value = !isOpenAside.value
isOpenAside.value = !isOpenAside.value
}
</script>
\ No newline at end of file
<template>
<div class="flex flex-col w-full gap-2">
<UButton
leading-icon="i-heroicons-home"
color="gray"
variant="ghost"
size="md"
label="首页"
to="/"
/>
<UButton
leading-icon="i-heroicons-rectangle-stack"
color="gray"
variant="ghost"
size="md"
label="主题"
to="/library"
/>
<ISearchHistory @sign="$openSign" />
<ClientOnly>
<UButton
v-if="!$isSignIn"
class="flex gap-2 justify-center"
size="lg"
label="登录"
@click="$openSign"
/>
</ClientOnly>
</div>
<UModal v-model="$isOpenSign">
<ISign @close="$closeSign" />
</UModal>
<div class="flex flex-col w-full gap-2">
<UButton
leading-icon="i-heroicons-home"
color="gray"
variant="ghost"
size="md"
label="首页"
to="/"
/>
<UButton
leading-icon="i-heroicons-rectangle-stack"
color="gray"
variant="ghost"
size="md"
label="主题"
to="/library"
/>
<ISearchHistory @sign="$openSign" />
<ClientOnly>
<UButton
v-if="!$isSignIn"
class="flex gap-2 justify-center"
size="lg"
label="登录"
@click="$openSign"
/>
</ClientOnly>
</div>
<UModal v-model="$isOpenSign">
<ISign @close="$closeSign" />
</UModal>
</template>
<script setup>
const { $isSignIn, $isOpenSign } = storeToRefs(useUserStore())
......
<template>
<div class="flex flex-col overflow-hidden group">
<div class="flex justify-between">
<UButton
class="flex-grow cursor-default hover:bg-transparent dark:hover:bg-transparent"
leading-icon="i-heroicons-queue-list"
color="gray"
variant="ghost"
size="md"
label="浏览记录"
/>
<UButton
class="hidden group-hover:flex"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</div>
<ClientOnly>
<div v-auto-animate class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5">
<template v-for="item in $searchHistory.reverse()" :key="item.c_id">
<UButton
class="flex text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate" :title="item.title">{{ item.title }}</div>
</UButton>
</template>
</div>
</ClientOnly>
</div>
<div class="flex flex-col overflow-hidden group">
<div class="flex justify-between">
<UButton
class="flex-grow cursor-default hover:bg-transparent dark:hover:bg-transparent"
leading-icon="i-heroicons-queue-list"
color="gray"
variant="ghost"
size="md"
label="浏览记录"
/>
<UButton
class="hidden group-hover:flex"
label="清空"
size="md"
variant="link"
@click="handleClear"
/>
</div>
<ClientOnly>
<div v-auto-animate class="flex overflow-y-auto flex-col gap-1 border-l border-gray-200 dark:border-gray-800 pl-2 ml-5">
<template v-for="item in $searchHistory.reverse()" :key="item.c_id">
<UButton
class="flex text-gray-400"
color="gray"
variant="ghost"
size="xs"
long
:to="`/search/${item.c_id}`"
>
<div class="flex-grow truncate" :title="item.title">{{ item.title }}</div>
</UButton>
</template>
</div>
</ClientOnly>
</div>
</template>
<script setup>
const emits = defineEmits(['sign', 'clear'])
const modal = useModal()
const { $searchHistory } = storeToRefs(useSearchStore())
const { $clearSearchHistory } = useSearchStore()
const query = ref('')
function handleClear () {
$clearSearchHistory()
$clearSearchHistory()
}
</script>
<template>
<div class="flex flex-col items-start gap-4 p-4">
<div class="flex w-full justify-between">
<ILogo />
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-x-mark-20-solid"
@click="handleClose"
/>
</div>
<div>登录以继续使用</div>
<UButton
block
color="gray"
size="md"
@click="handleGetSignUrl('gitcode')"
>
<img src="~/assets/svg/logo-gitcode.svg" />
使用 GitCode 登录
</UButton>
<UButton
block
leading-icon="i-simple-icons-github"
label="使用 GitHub 登录"
color="gray"
size="md"
@click="handleGetSignUrl('github')"
/>
<UButton
block
leading-icon="i-simple-icons-google"
label="使用 Google 登录"
color="gray"
size="md"
disabled
/>
<!-- <UDivider label="或" />-->
<!-- <UInput-->
<!-- class="w-full"-->
<!-- v-model="email"-->
<!-- placeholder="输入邮箱地址..."-->
<!-- size="md"-->
<!-- />-->
<!-- <UButton-->
<!-- block-->
<!-- leading-icon="i-heroicons-envelope-20-solid"-->
<!-- label="邮箱登录"-->
<!-- size="md"-->
<!-- @click="handleSign"-->
<!-- />-->
</div>
<div class="flex flex-col items-start gap-4 p-4">
<div class="flex w-full justify-between">
<ILogo />
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-x-mark-20-solid"
@click="handleClose"
/>
</div>
<div>登录以继续使用</div>
<UButton
block
color="gray"
size="md"
@click="handleGetSignUrl('gitcode')"
>
<img src="~/assets/svg/logo-gitcode.svg" >
使用 GitCode 登录
</UButton>
<UButton
block
leading-icon="i-simple-icons-github"
label="使用 GitHub 登录"
color="gray"
size="md"
@click="handleGetSignUrl('github')"
/>
<UButton
block
leading-icon="i-simple-icons-google"
label="使用 Google 登录"
color="gray"
size="md"
disabled
/>
<!-- <UDivider label="或" />-->
<!-- <UInput-->
<!-- class="w-full"-->
<!-- v-model="email"-->
<!-- placeholder="输入邮箱地址..."-->
<!-- size="md"-->
<!-- />-->
<!-- <UButton-->
<!-- block-->
<!-- leading-icon="i-heroicons-envelope-20-solid"-->
<!-- label="邮箱登录"-->
<!-- size="md"-->
<!-- @click="handleSign"-->
<!-- />-->
</div>
</template>
<script setup>
const emits = defineEmits(['close', 'signIn'])
const email = ref('')
// const email = ref('')
function handleClose () {
emits('close')
}
function handleSign () {
emits('signIn')
emits('close')
}
// function handleSign () {
// emits('signIn')
// }
async function handleGetSignUrl (source) {
let url
if (source === 'github') {
const { data } = await useRequest('/v1/user/github/authorize_url')
url = data.value.data.url
} else if (source === 'gitcode') {
const { data } = await useRequest('/v1/user/gitcode/authorize_url')
url = data.value.data.url
}
window.location.href = url
let url
if (source === 'github') {
const { data } = await useRequest('/v1/user/github/authorize_url')
url = data.value.data.url
} else if (source === 'gitcode') {
const { data } = await useRequest('/v1/user/gitcode/authorize_url')
url = data.value.data.url
}
window.location.href = url
}
</script>
<template>
<div class="flex flex-col w-full">
<ClientOnly>
<template v-if="$isSignIn">
<div class="flex flex-grow justify-between items-center p-4">
<UDropdown class="flex flex-grow" :items="items">
<UButton class="flex flex-grow" color="gray" variant="ghost">
<div class="flex flex-grow items-center gap-2">
<UAvatar :src="$info.avatar_url" />
<div>{{ $info.nickname }}</div>
<UBadge v-if="$info.pro" size="xs" color="yellow" variant="soft" label="PRO" />
<UBadge v-else size="xs" color="gray" variant="soft" label="FREE" />
</div>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-lg" />
</UButton>
</UDropdown>
</div>
<UDivider />
</template>
</ClientOnly>
<div class="flex justify-between gap-2 p-4">
<UButton
color="gray"
variant="ghost"
label="问题反馈"
/>
<ClientOnly>
<div class="flex">
<ColorPicker />
<USelectMenu
v-model="colorMode.preference"
:ui-menu="{ width: 'w-32' }"
:options="themeItems"
value-attribute="value"
>
<UButton
color="gray"
variant="ghost"
square
:icon="colorMode.value === 'dark' ? 'i-heroicons-moon-16-solid' : 'i-heroicons-sun-16-solid'"
aria-label="Theme"
/>
</USelectMenu>
<USelectMenu
v-if="false"
:ui-menu="{ width: 'w-32' }"
:model-value="$lang"
:options="$langOptions"
value-attribute="value"
>
<UButton
icon="i-heroicons-language-16-solid"
color="gray"
variant="ghost"
/>
</USelectMenu>
</div>
</ClientOnly>
</div>
</div>
<div class="flex flex-col w-full">
<ClientOnly>
<template v-if="$isSignIn">
<div class="flex flex-grow justify-between items-center p-4">
<UDropdown class="flex flex-grow" :items="items">
<UButton class="flex flex-grow" color="gray" variant="ghost">
<div class="flex flex-grow items-center gap-2">
<UAvatar :src="$info.avatar_url" />
<div>{{ $info.nickname }}</div>
<UBadge v-if="$info.pro" size="xs" color="yellow" variant="soft" label="PRO" />
<UBadge v-else size="xs" color="gray" variant="soft" label="FREE" />
</div>
<UIcon name="i-heroicons-chevron-down-20-solid" class="text-lg" />
</UButton>
</UDropdown>
</div>
<UDivider />
</template>
</ClientOnly>
<div class="flex justify-between gap-2 p-4">
<UButton
color="gray"
variant="ghost"
label="问题反馈"
/>
<ClientOnly>
<div class="flex">
<ColorPicker />
<USelectMenu
v-model="colorMode.preference"
:ui-menu="{ width: 'w-32' }"
:options="themeItems"
value-attribute="value"
>
<UButton
color="gray"
variant="ghost"
square
:icon="colorMode.value === 'dark' ? 'i-heroicons-moon-16-solid' : 'i-heroicons-sun-16-solid'"
aria-label="Theme"
/>
</USelectMenu>
<USelectMenu
v-if="false"
:ui-menu="{ width: 'w-32' }"
:model-value="$lang"
:options="$langOptions"
value-attribute="value"
>
<UButton
icon="i-heroicons-language-16-solid"
color="gray"
variant="ghost"
/>
</USelectMenu>
</div>
</ClientOnly>
</div>
</div>
</template>
<script setup>
const { $signOut } = useUserStore()
......@@ -65,41 +65,41 @@ const { $isSignIn, $info } = storeToRefs(useUserStore())
const { $lang, $langOptions } = useI18nStore()
const colorMode = useColorMode()
const items = [
[
{
label: '账号信息',
icon: 'i-heroicons-user'
},
{
label: '用户反馈',
icon: 'i-heroicons-chat-bubble-oval-left-ellipsis'
}
],
[
{
label: '退出登录',
icon: 'i-heroicons-power',
click: () => {
$signOut()
}
}
]
]
const themeItems = [
[
{
label: '亮色模式',
value: 'light',
icon: 'i-heroicons-sun'
label: '账号信息',
icon: 'i-heroicons-user'
},
{
label: '深色模式',
value: 'dark',
icon: 'i-heroicons-moon'
},
label: '用户反馈',
icon: 'i-heroicons-chat-bubble-oval-left-ellipsis'
}
],
[
{
label: '跟随系统',
value: 'system',
icon: 'i-heroicons-computer-desktop'
label: '退出登录',
icon: 'i-heroicons-power',
click: () => {
$signOut()
}
}
]
]
const themeItems = [
{
label: '亮色模式',
value: 'light',
icon: 'i-heroicons-sun'
},
{
label: '深色模式',
value: 'dark',
icon: 'i-heroicons-moon'
},
{
label: '跟随系统',
value: 'system',
icon: 'i-heroicons-computer-desktop'
}
]
</script>
<template>
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
<ILibraryEdit :id="id" ref="refEdit" />
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
<ILibraryEdit :id="id" ref="refEdit" />
</template>
<script setup>
import { IConfirm } from '#components'
const { deleteCollection } = useCollectionRequest()
const modal = useModal()
const props = defineProps({
id: {
type: [String, Number],
default: ''
}
id: {
type: [String, Number],
default: ''
}
})
const actionItems = [
[
{
label: '编辑合集',
icon: 'i-heroicons-pencil-square',
click: () => {
handleOpen()
}
},
{
label: '删除合集',
icon: 'i-heroicons-trash',
click: () => {
handleOpenDelete()
}
}
]
[
{
label: '编辑合集',
icon: 'i-heroicons-pencil-square',
click: () => {
handleOpen()
}
},
{
label: '删除合集',
icon: 'i-heroicons-trash',
click: () => {
handleOpenDelete()
}
}
]
]
const refEdit = ref(null)
function handleOpen () {
refEdit.value.open()
refEdit.value.open()
}
function handleOpenDelete () {
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该合集吗?',
async onSuccess() {
await deleteCollection(props.id)
modal.close()
navigateTo('/library')
},
onCancel () {
modal.close()
}
})
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该合集吗?',
async onSuccess() {
await deleteCollection(props.id)
modal.close()
navigateTo('/library')
},
onCancel () {
modal.close()
}
})
}
</script>
<template>
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
:size="size"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
<UDropdown class="flex flex-grow" :items="actionItems">
<UButton
color="gray"
variant="ghost"
:size="size"
square
icon="i-heroicons-ellipsis-horizontal"
/>
</UDropdown>
</template>
<script setup>
import { IConfirm } from '#components'
......@@ -16,73 +16,73 @@ const { deleteCollectionRecord, deleteThread } = useCollectionRequest()
const Layout = inject('Layout')
const modal = useModal()
const props = defineProps({
collection_id: {
type: [String, Number],
default: ''
},
c_id: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'sm'
}
collectionId: {
type: [String, Number],
default: ''
},
cId: {
type: [String, Number],
default: ''
},
size: {
type: String,
default: 'sm'
}
})
const emit = defineEmits(['delete'])
const actionItems = computed(() => {
let items
if (props.collection_id && props.c_id) {
items = [
{
label: '更改合集',
icon: 'i-heroicons-squares-plus',
click: () => {
$openLibrarySelect(props.c_id, [props.collection_id])
}
},
{
label: '从收藏中移除',
icon: 'i-heroicons-x-mark',
click: async () => {
await deleteCollectionRecord(props.collection_id, props.c_id)
Layout.handleRemoveCollectData({
collection_id: props.collection_id,
c_id: props.c_id
})
}
}
]
} else {
items = [
{
label: '添加到收藏',
icon: 'i-heroicons-plus',
click: () => {
$openLibrarySelect(props.c_id)
}
}
]
}
items.push({
label: '删除主题',
icon: 'i-heroicons-trash',
let items
if (props.collection_id && props.c_id) {
items = [
{
label: '更改合集',
icon: 'i-heroicons-squares-plus',
click: () => {
$openLibrarySelect(props.c_id, [props.collection_id])
}
},
{
label: '从收藏中移除',
icon: 'i-heroicons-x-mark',
click: async () => {
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该主题吗?',
async onSuccess() {
modal.close()
await deleteThread([props.c_id])
$getCollection()
emit('delete')
},
onCancel () {
modal.close()
}
})
await deleteCollectionRecord(props.collection_id, props.c_id)
Layout.handleRemoveCollectData({
collection_id: props.collection_id,
c_id: props.c_id
})
}
})
return [items]
}
]
} else {
items = [
{
label: '添加到收藏',
icon: 'i-heroicons-plus',
click: () => {
$openLibrarySelect(props.c_id)
}
}
]
}
items.push({
label: '删除主题',
icon: 'i-heroicons-trash',
click: async () => {
modal.open(IConfirm, {
title: '删除确认',
description: '确定要删除该主题吗?',
async onSuccess() {
modal.close()
await deleteThread([props.c_id])
$getCollection()
emit('delete')
},
onCancel () {
modal.close()
}
})
}
})
return [items]
})
</script>
<template>
<ULink :to="`/library/${item.id}`">
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<div>{{ item.name }}</div>
<div class="flex">
<div class="flex items-center text-gray-500 text-sm gap-0.5">
<UIcon name="i-heroicons-square-3-stack-3d" />
<span>{{ item.record_count }}</span>
</div>
</div>
</div>
</UCard>
</ULink>
<ULink :to="`/library/${item.id}`">
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<div>{{ item.name }}</div>
<div class="flex">
<div class="flex items-center text-gray-500 text-sm gap-0.5">
<UIcon name="i-heroicons-square-3-stack-3d" />
<span>{{ item.record_count }}</span>
</div>
</div>
</div>
</UCard>
</ULink>
</template>
<script setup>
defineProps({
item: {
type: Object,
default: (() => {})()
}
item: {
type: Object,
default: (() => {})()
}
})
const cardUI = {
body: {
padding: 'sm:p-3 p-3',
base: 'h-full'
},
background: 'transition bg-gray-50 hover:bg-gray-100 dark:hover:bg-gray-800'
body: {
padding: 'sm:p-3 p-3',
base: 'h-full'
},
background: 'transition bg-gray-50 hover:bg-gray-100 dark:hover:bg-gray-800'
}
</script>
\ No newline at end of file
<template>
<UModal
:model-value="!id ? $isLibraryCreateOpen : updateVisible"
@update:modelValue="handleCloseModal"
:ui="{ width: 'w-96 sm:max-w-screen-md' }"
>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<div class="text-xl" v-if="!id">创建合集</div>
<div class="text-xl" v-else>更新合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">标题</label>
</div>
<UInput
v-model="title"
placeholder="合集标题"
autofocus
/>
</div>
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">
描述
<span class="text-gray-400 dark:text-gray-100">(可选)</span>
</label>
</div>
<UTextarea
v-model="description"
placeholder="合集描述"
/>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
v-if="!id"
size="md"
label="创建"
:loading="loading"
:disabled="!title"
@click="handleCreate"
/>
<UButton
v-else
size="md"
label="更新"
:loading="loading"
:disabled="!title"
@click="handleUpdate"
/>
</div>
</template>
</UCard>
</UModal>
<UModal
:model-value="!id ? $isLibraryCreateOpen : updateVisible"
:ui="{ width: 'w-96 sm:max-w-screen-md' }"
@update:model-value="handleCloseModal"
>
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<div v-if="!id" class="text-xl">创建合集</div>
<div v-else class="text-xl">更新合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-6">
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">标题</label>
</div>
<UInput
v-model="title"
placeholder="合集标题"
autofocus
/>
</div>
<div class="flex flex-col gap-1">
<div class="flex content-center items-center justify-between text-sm">
<label class="block font-medium text-gray-700 dark:text-gray-200">
描述
<span class="text-gray-400 dark:text-gray-100">(可选)</span>
</label>
</div>
<UTextarea
v-model="description"
placeholder="合集描述"
/>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
v-if="!id"
size="md"
label="创建"
:loading="loading"
:disabled="!title"
@click="handleCreate"
/>
<UButton
v-else
size="md"
label="更新"
:loading="loading"
:disabled="!title"
@click="handleUpdate"
/>
</div>
</template>
</UCard>
</UModal>
</template>
<script setup>
const { $isLibraryCreateOpen, $collection } = storeToRefs(useLibraryStore())
const { $closeLibraryCreate, $getCollection } = useLibraryStore()
const { setOrUpdateCollection } = useCollectionRequest()
const props = defineProps({
id: {
type: [String, Number],
default: ''
}
id: {
type: [String, Number],
default: ''
}
})
const updateVisible = ref(false)
const title = ref('')
const description = ref('')
const loading = ref(false)
function handleClose () {
if (!props.id) $closeLibraryCreate()
else updateVisible.value = false
if (!props.id) $closeLibraryCreate()
else updateVisible.value = false
}
async function handleCreate () {
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
title.value = ''
description.value = ''
$closeLibraryCreate()
$getCollection()
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
title.value = ''
description.value = ''
$closeLibraryCreate()
$getCollection()
}
function handleCloseModal () {
if (!props.id) $isLibraryCreateOpen.value = false
else updateVisible.value = false
if (!props.id) $isLibraryCreateOpen.value = false
else updateVisible.value = false
}
async function handleUpdate () {
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
id: Number(props.id),
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
handleCloseModal()
$getCollection()
if (loading.value) return
loading.value = true
const { error } = await setOrUpdateCollection({
id: Number(props.id),
name: title.value,
description: description.value
})
loading.value = false
if (error.value) return
handleCloseModal()
$getCollection()
}
function handleFillInfo () {
const collection =$collection.value
const id = Number(props.id)
const { name, description:currentDes } = collection.find(item => item.id === id)
if (name) {
title.value = name
description.value = currentDes
}
const collection =$collection.value
const id = Number(props.id)
const { name, description:currentDes } = collection.find(item => item.id === id)
if (name) {
title.value = name
description.value = currentDes
}
}
function openUpdate () {
updateVisible.value = true
// todo 获取合集数据
if (props.id) {
handleFillInfo()
}
updateVisible.value = true
// todo 获取合集数据
if (props.id) {
handleFillInfo()
}
}
defineExpose({
openUpdate
openUpdate
})
</script>
<template>
<ILibraryCreate :id="id" ref="refCreate" />
<ILibraryCreate :id="id" ref="refCreate" />
</template>
<script setup>
defineProps({
id: {
type: [String, Number],
default: ''
}
id: {
type: [String, Number],
default: ''
}
})
const refCreate = ref(null)
function open () {
refCreate.value.openUpdate()
refCreate.value.openUpdate()
}
defineExpose({
open
open
})
</script>
\ No newline at end of file
<template>
<div class="flex flex-col w-full items-center sticky top-0 bg-white dark:bg-black z-10">
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6">
<div class="flex justify-between items-center gap-4">
<div class="flex gap-4">
<IMenuSider />
<div class="flex flex-shrink-0 items-center text-xl gap-2" v-if="!collect">
<UIcon name="i-heroicons-rectangle-stack-20-solid" />
<div>主题</div>
</div>
<template v-else>
<UButton
icon="i-heroicons-chevron-left"
color="gray"
variant="ghost"
to="/library"
/>
<div class="flex items-center text-xl gap-2">
<UIcon name="i-heroicons-squares-2x2" />
<div>{{ collect }}</div>
<UBadge color="gray" variant="soft" :label="count" />
</div>
</template>
</div>
<div class="flex flex-grow justify-end items-center gap-4">
<div class="flex">
<IActionCollect :id="collectId" v-if="collect" />
</div>
<div class="flex" v-if="!collect">
<UInput
name="queryInput"
:ui="{ icon: { trailing: { pointer: '' } } }"
v-model="searchQuery"
:loading="searchLoading"
icon="i-heroicons-magnifying-glass-20-solid"
placeholder="搜索你的主题..."
size="md"
>
<template #trailing>
<UButton
v-show="searchQuery !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="handleClear"
/>
</template>
</UInput>
</div>
</div>
</div>
<div v-if="description" class="text-gray-500 ml-12 mt-2">{{ description }}</div>
</div>
<UDivider />
<div v-if="showTabs" class="w-full p-6 block lg:hidden">
<UTabs :model-value="tab" @update:modelValue="handleChangeTab" :items="tabs" />
</div>
</div>
<div class="flex flex-col w-full items-center sticky top-0 bg-white dark:bg-black z-10">
<div class="container max-w-screen-lg 2xl:max-w-screen-xl flex flex-col p-6">
<div class="flex justify-between items-center gap-4">
<div class="flex gap-4">
<IMenuSider />
<div v-if="!collect" class="flex flex-shrink-0 items-center text-xl gap-2">
<UIcon name="i-heroicons-rectangle-stack-20-solid" />
<div>主题</div>
</div>
<template v-else>
<UButton
icon="i-heroicons-chevron-left"
color="gray"
variant="ghost"
to="/library"
/>
<div class="flex items-center text-xl gap-2">
<UIcon name="i-heroicons-squares-2x2" />
<div>{{ collect }}</div>
<UBadge color="gray" variant="soft" :label="count" />
</div>
</template>
</div>
<div class="flex flex-grow justify-end items-center gap-4">
<div class="flex">
<IActionCollect v-if="collect" :id="collectId" />
</div>
<div v-if="!collect" class="flex">
<UInput
v-model="searchQuery"
name="queryInput"
:ui="{ icon: { trailing: { pointer: '' } } }"
:loading="searchLoading"
icon="i-heroicons-magnifying-glass-20-solid"
placeholder="搜索你的主题..."
size="md"
>
<template #trailing>
<UButton
v-show="searchQuery !== ''"
color="gray"
variant="link"
icon="i-heroicons-x-mark-20-solid"
:padded="false"
@click="handleClear"
/>
</template>
</UInput>
</div>
</div>
</div>
<div v-if="description" class="text-gray-500 ml-12 mt-2">{{ description }}</div>
</div>
<UDivider />
<div v-if="showTabs" class="w-full p-6 block lg:hidden">
<UTabs :model-value="tab" :items="tabs" @update:model-value="handleChangeTab" />
</div>
</div>
</template>
<script setup>
defineProps({
collect: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
count: {
type: Number,
default: 0
},
collectId: {
type: [String, Number],
default: ''
},
showTabs: {
type: Boolean,
default: false
},
tab: {
type: Number,
default: 0
}
collect: {
type: String,
default: ''
},
description: {
type: String,
default: ''
},
count: {
type: Number,
default: 0
},
collectId: {
type: [String, Number],
default: ''
},
showTabs: {
type: Boolean,
default: false
},
tab: {
type: Number,
default: 0
}
})
const emit = defineEmits(['search', 'clear', 'change-tab'])
const searchQuery = ref('')
const searchLoading = ref(false)
defineShortcuts({
enter: {
usingInput: 'queryInput',
handler: async () => {
searchLoading.value = true
const { data } = await useRequest(`/v1/chat/completion/list?keyword=${searchQuery.value}`)
searchLoading.value = false
emit('search', data.value.data)
}
enter: {
usingInput: 'queryInput',
handler: async () => {
searchLoading.value = true
const { data } = await useRequest(`/v1/chat/completion/list?keyword=${searchQuery.value}`)
searchLoading.value = false
emit('search', data.value.data)
}
}
})
function handleClear () {
searchQuery.value = ''
emit('clear')
searchQuery.value = ''
emit('clear')
}
const tabs = [
{
label: '全部主题',
icon: 'i-heroicons-square-3-stack-3d'
},
{
label: '合集'
}
{
label: '全部主题',
icon: 'i-heroicons-square-3-stack-3d'
},
{
label: '合集'
}
]
function handleChangeTab (index) {
emit('change-tab', index)
emit('change-tab', index)
}
</script>
<template>
<UModal v-model="$isLibrarySelectOpen" :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<div class="text-xl">选择合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-4">
<UButton
leading-icon="i-heroicons-plus-20-solid"
label="创建新合集"
color="gray"
@click="handleOpenCreate"
/>
<div class="flex flex-col gap-2">
<UButton
v-for="item in $collection"
color="white"
size="md"
class="flex"
:key="item.id"
@click="handleSelected(item.id)"
>
<div class="flex flex-grow justify-between items-center">
<div>{{ item.name }}</div>
<UIcon v-if="selected.includes(item.id)" name="i-heroicons-check-circle-20-solid" class="text-primary text-lg" />
</div>
</UButton>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
size="md"
label="保存"
:loading="loading"
@click="handleSave"
/>
</div>
</template>
</UCard>
</UModal>
<UModal v-model="$isLibrarySelectOpen" :ui="{ width: 'w-96 sm:max-w-screen-md' }">
<UCard :ui="{ ring: '', divide: 'divide-y divide-gray-100 dark:divide-gray-800' }">
<template #header>
<div class="flex items-center justify-between">
<div class="text-xl">选择合集</div>
<UButton
leading-icon="i-heroicons-x-mark-20-solid"
color="gray"
variant="ghost"
@click="handleClose"
/>
</div>
</template>
<div class="flex flex-col gap-4">
<UButton
leading-icon="i-heroicons-plus-20-solid"
label="创建新合集"
color="gray"
@click="handleOpenCreate"
/>
<div class="flex flex-col gap-2">
<UButton
v-for="item in $collection"
:key="item.id"
color="white"
size="md"
class="flex"
@click="handleSelected(item.id)"
>
<div class="flex flex-grow justify-between items-center">
<div>{{ item.name }}</div>
<UIcon v-if="selected.includes(item.id)" name="i-heroicons-check-circle-20-solid" class="text-primary text-lg" />
</div>
</UButton>
</div>
</div>
<template #footer>
<div class="flex justify-end">
<UButton
size="md"
label="保存"
:loading="loading"
@click="handleSave"
/>
</div>
</template>
</UCard>
</UModal>
</template>
<script setup>
const { $isLibrarySelectOpen, $selectCollectionId, $selectThreadId, $collection } = storeToRefs(useLibraryStore())
......@@ -57,43 +57,43 @@ const emits = defineEmits(['success'])
const selected = ref([])
const loading = ref(false)
function handleClose () {
$closeLibrarySelect()
$closeLibrarySelect()
}
function handleOpenCreate () {
handleClose()
$openLibraryCreate()
handleClose()
$openLibraryCreate()
}
async function handleSelected(id) {
selected.value = [id]
selected.value = [id]
}
async function handleSave() {
if (loading.value) return
loading.value = true
// 取消合集
const hasSelected = $selectCollectionId.value
if (hasSelected.length) {
const { error } = await deleteCollectionRecord(hasSelected[0], $selectThreadId.value)
!error.value && $setSelectCollectionId([])
}
// 添加合集
if (selected.value.length) {
const selectedItem = selected.value[0]
const { error } = await saveCollection({ collection_id: selectedItem, c_id: $selectThreadId.value })
!error.value && $setSelectCollectionId([selectedItem])
}
loading.value = false
handleClose()
$getCollection()
emits('success', {
c_id: $selectThreadId.value,
collection_id: selected.value[0],
collection_name: $collection.value.find(i => i.id === selected.value[0]).name
})
if (loading.value) return
loading.value = true
// 取消合集
const hasSelected = $selectCollectionId.value
if (hasSelected.length) {
const { error } = await deleteCollectionRecord(hasSelected[0], $selectThreadId.value)
!error.value && $setSelectCollectionId([])
}
// 添加合集
if (selected.value.length) {
const selectedItem = selected.value[0]
const { error } = await saveCollection({ collection_id: selectedItem, c_id: $selectThreadId.value })
!error.value && $setSelectCollectionId([selectedItem])
}
loading.value = false
handleClose()
$getCollection()
emits('success', {
c_id: $selectThreadId.value,
collection_id: selected.value[0],
collection_name: $collection.value.find(i => i.id === selected.value[0]).name
})
}
watch(() => $isLibrarySelectOpen.value, () => {
selected.value = [...$selectCollectionId.value]
if (!$collection.value.length) {
$getCollection()
}
selected.value = [...$selectCollectionId.value]
if (!$collection.value.length) {
$getCollection()
}
})
</script>
<template>
<ULink :to="`/search/${thread.c_id}`" class="flex flex-col group">
<div class="flex items-center gap-2 transition group-hover:text-primary">{{ thread.title }}</div>
<div class="break-word text-balance line-clamp-2 font-sans text-sm" :class="textColor" v-if="false">
{{ thread.description }}
</div>
</ULink>
<div class="flex justify-between items-center">
<div class="flex gap-4">
<UTooltip class="flex items-center text-sm gap-0.5" :class="textColor" :text="thread.is_public ? '公开主题,链接可被发现' : '私密主题,仅自己可见'">
<UIcon :name="thread.is_public ? 'i-heroicons-lock-open' : 'i-heroicons-lock-closed'" />
<span>{{ thread.is_public ? '公开' : '私有' }}</span>
</UTooltip>
<!-- <div class="flex items-center text-sm gap-0.5" :class="textColor">-->
<!-- <UIcon name="i-heroicons-eye" />-->
<!-- <span>1</span>-->
<!-- </div>-->
<div class="flex" :class="textColor">
<UTooltip class="flex items-center text-sm gap-0.5">
<UIcon name="i-heroicons-clock" />
<span>{{ useTime(thread.create_time) }}</span>
<template #text>
{{ toValue(useDateFormat(thread.create_time, 'YYYY年M月D日 HH:mm')) }}
</template>
</UTooltip>
</div>
</div>
<div class="flex gap-4">
<template v-for="collect in thread.collections" :key="collect.collection_id">
<UButton
:ui="{ rounded: 'rounded-full' }"
:to="`/library/${collect.collection_id}`"
color="white"
size="2xs"
:label="collect.collection_name"
/>
</template>
<UTooltip text="添加到收藏" v-if="!thread.collections.length">
<UButton
color="gray"
variant="ghost"
size="2xs"
square
icon="i-heroicons-plus"
@click="handleOpenSelect"
/>
</UTooltip>
<IActionThread
:collection_id="thread.collections.length ? thread.collections[0].collection_id : ''"
:c_id="item.c_id"
size="2xs"
@delete="handleDeletedThread"
/>
</div>
</div>
<UDivider />
<ULink :to="`/search/${thread.c_id}`" class="flex flex-col group">
<div class="flex items-center gap-2 transition group-hover:text-primary">{{ thread.title }}</div>
<div v-if="false" class="break-word text-balance line-clamp-2 font-sans text-sm" :class="textColor">
{{ thread.description }}
</div>
</ULink>
<div class="flex justify-between items-center">
<div class="flex gap-4">
<UTooltip class="flex items-center text-sm gap-0.5" :class="textColor" :text="thread.is_public ? '公开主题,链接可被发现' : '私密主题,仅自己可见'">
<UIcon :name="thread.is_public ? 'i-heroicons-lock-open' : 'i-heroicons-lock-closed'" />
<span>{{ thread.is_public ? '公开' : '私有' }}</span>
</UTooltip>
<!-- <div class="flex items-center text-sm gap-0.5" :class="textColor">-->
<!-- <UIcon name="i-heroicons-eye" />-->
<!-- <span>1</span>-->
<!-- </div>-->
<div class="flex" :class="textColor">
<UTooltip class="flex items-center text-sm gap-0.5">
<UIcon name="i-heroicons-clock" />
<span>{{ useTime(thread.create_time) }}</span>
<template #text>
{{ toValue(useDateFormat(thread.create_time, 'YYYY年M月D日 HH:mm')) }}
</template>
</UTooltip>
</div>
</div>
<div class="flex gap-4">
<template v-for="collect in thread.collections" :key="collect.collection_id">
<UButton
:ui="{ rounded: 'rounded-full' }"
:to="`/library/${collect.collection_id}`"
color="white"
size="2xs"
:label="collect.collection_name"
/>
</template>
<UTooltip v-if="!thread.collections.length" text="添加到收藏">
<UButton
color="gray"
variant="ghost"
size="2xs"
square
icon="i-heroicons-plus"
@click="handleOpenSelect"
/>
</UTooltip>
<IActionThread
:collection_id="thread.collections.length ? thread.collections[0].collection_id : ''"
:c_id="item.c_id"
size="2xs"
@delete="handleDeletedThread"
/>
</div>
</div>
<UDivider />
</template>
<script setup>
const Layout = inject('Layout')
const { $openLibrarySelect } = useLibraryStore()
const textColor = 'text-gray-500 dark:text-gray-400'
const props = defineProps({
item: {
type: Object,
default: (() => {})()
},
isItem: {
type: Boolean,
default: false
}
item: {
type: Object,
default: (() => {})()
},
isItem: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['delete'])
const thread = computed(() => {
if (!props.isItem) return props.item
else {
const item = props.item
item.title = props.item.c_title
item.collections = []
if (props.item.collection_id && props.item.collection_name) {
item.collections = [
{
collection_id: props.item.collection_id,
collection_name: props.item.collection_name
}
]
if (!props.isItem) return props.item
else {
const item = props.item
item.title = props.item.c_title
item.collections = []
if (props.item.collection_id && props.item.collection_name) {
item.collections = [
{
collection_id: props.item.collection_id,
collection_name: props.item.collection_name
}
return item
]
}
return item
}
})
function handleOpenSelect () {
$openLibrarySelect(props.item.c_id)
$openLibrarySelect(props.item.c_id)
}
function handleUpdateCollect (data) {
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = [data.value]
Layout.handleClearCollectData()
}
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = [data.value]
Layout.handleClearCollectData()
}
}
function handleRemoveCollect (data) {
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = []
Layout.handleClearRemoveCollectData()
}
const { c_id } = data.value
if (c_id === thread.value.c_id) {
thread.value.collections = []
Layout.handleClearRemoveCollectData()
}
}
watch(()=> Layout.selectCollectData, (data) => {
if (data.value !== null) handleUpdateCollect(data)
if (data.value !== null) handleUpdateCollect(data)
}, { deep: true })
watch(()=> Layout.removeCollectData, (data) => {
if (data.value !== null) handleRemoveCollect(data)
if (data.value !== null) handleRemoveCollect(data)
}, { deep: true })
function handleDeletedThread () {
emit('delete', thread.value.c_id)
emit('delete', thread.value.c_id)
}
</script>
\ No newline at end of file
<template>
<div class="grid grid-cols-1">
<MDC class="prose dark:prose-invert max-w-none" :class="'prose-' + size" v-if="content" :value="content" tag="article" />
</div>
<div class="grid grid-cols-1">
<MDC v-if="content" class="prose dark:prose-invert max-w-none" :class="'prose-' + size" :value="content" tag="article" />
</div>
</template>
<script setup>
defineProps({
content: {
type: String,
default: ''
},
size: {
type: String,
default: 'base'
}
content: {
type: String,
default: ''
},
size: {
type: String,
default: 'base'
}
})
</script>
\ No newline at end of file
<template>
<div class="flex flex-col lg:flex-row gap-6 space-x-0 lg:space-x-6">
<div class="flex flex-grow flex-col gap-6">
<div class="grid">
<slot name="title" />
</div>
<slot />
</div>
<div class="flex flex-col w-full lg:w-64 flex-shrink-0 gap-6" v-if="$slots.extra">
<slot name="extra" />
</div>
</div>
<div class="flex flex-col lg:flex-row gap-6 space-x-0 lg:space-x-6">
<div class="flex flex-grow flex-col gap-6">
<div class="grid">
<slot name="title" />
</div>
<slot />
</div>
<div v-if="$slots.extra" class="flex flex-col w-full lg:w-64 flex-shrink-0 gap-6">
<slot name="extra" />
</div>
</div>
</template>
<script setup>
......
<template>
<div class="flex w-full justify-center">
<UCard
class="hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI"
>
<UTextarea
ref="queryInput"
class="flex-grow"
name="queryInput"
:rows="1"
:maxrows="10"
autoresize
v-model="continueQuestion"
:placeholder="placeholder"
size="xl"
:padded="false"
variant="none"
maxlength="2000"
@focus="handleInputFocus"
@blur="handleInputBlur"
/>
<div class="flex flex-shrink-0 gap-2">
<UButton
v-if="!asking"
:ui="{ rounded: 'rounded-full' }"
:disabled="!continueQuestion"
trailing-icon="i-heroicons-chevron-right-20-solid"
size="xl"
@click.stop="handleAsk"
/>
<UTooltip v-else text="停止生成">
<UButton
:ui="{ rounded: 'rounded-full' }"
color="red"
trailing-icon="i-heroicons-stop-20-solid"
size="xl"
variant="ghost"
@click.stop="handleStop"
/>
</UTooltip>
</div>
</UCard>
</div>
<div class="flex w-full justify-center">
<UCard
class="hover:ring-2 has-[textarea:focus]:ring-2 has-[textarea:focus]:ring-primary-500 dark:has-[textarea:focus]:ring-primary-400"
:ui="cardUI"
>
<UTextarea
ref="queryInput"
v-model="continueQuestion"
class="flex-grow"
name="queryInput"
:rows="1"
:maxrows="10"
autoresize
:placeholder="placeholder"
size="xl"
:padded="false"
variant="none"
maxlength="2000"
@focus="handleInputFocus"
@blur="handleInputBlur"
/>
<div class="flex flex-shrink-0 gap-2">
<UButton
v-if="!asking"
:ui="{ rounded: 'rounded-full' }"
:disabled="!continueQuestion"
trailing-icon="i-heroicons-chevron-right-20-solid"
size="xl"
@click.stop="handleAsk"
/>
<UTooltip v-else text="停止生成">
<UButton
:ui="{ rounded: 'rounded-full' }"
color="red"
trailing-icon="i-heroicons-stop-20-solid"
size="xl"
variant="ghost"
@click.stop="handleStop"
/>
</UTooltip>
</div>
</UCard>
</div>
</template>
<script setup>
const { metaSymbol } = useShortcuts()
const { isDesktop } = useDevice()
const placeholder = computed(() => `提出后续问题${isDesktop ? '' + metaSymbol.value + 'L)' : ''}`)
const props = defineProps({
asking: {
type: Boolean,
default: false
}
asking: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['ask', 'stop'])
const isFocus = ref(false)
const cardUI = computed(() => {
const base = {
body: {
padding: '',
base: 'flex items-end has-[textarea[rows="1"]]:items-center pl-4 pr-1 py-1 gap-2'
},
// base: 'transition-[width] w-3/5 has-[textarea:focus]:w-full has-[button:focus]:w-full',
rounded: 'rounded has-[textarea[rows="1"]]:rounded-full'
}
if (isFocus.value) {
base.base = 'transition-[width] w-full'
} else {
base.base = 'transition-[width] w-full sm:w-3/5'
}
return base
const base = {
body: {
padding: '',
base: 'flex items-end has-[textarea[rows="1"]]:items-center pl-4 pr-1 py-1 gap-2'
},
// base: 'transition-[width] w-3/5 has-[textarea:focus]:w-full has-[button:focus]:w-full',
rounded: 'rounded has-[textarea[rows="1"]]:rounded-full'
}
if (isFocus.value) {
base.base = 'transition-[width] w-full'
} else {
base.base = 'transition-[width] w-full sm:w-3/5'
}
return base
})
defineShortcuts({
meta_enter: {
usingInput: 'queryInput',
handler: () => {
handleAsk()
}
},
escape: {
usingInput: 'queryInput',
handler: () => {
handleBlur()
}
},
meta_l: {
handler: () => {
handleFocus()
}
meta_enter: {
usingInput: 'queryInput',
handler: () => {
handleAsk()
}
},
escape: {
usingInput: 'queryInput',
handler: () => {
handleBlur()
}
},
meta_l: {
handler: () => {
handleFocus()
}
}
})
function handleFocus () {
queryInput.value.textarea.focus()
queryInput.value.textarea.focus()
}
function handleBlur () {
queryInput.value.textarea.blur()
queryInput.value.textarea.blur()
}
const continueQuestion = ref('')
function handleStop () {
emits('stop')
emits('stop')
}
function handleAsk () {
if (props.asking) return
emits('ask', continueQuestion.value)
continueQuestion.value = ''
handleBlur()
if (props.asking) return
emits('ask', continueQuestion.value)
continueQuestion.value = ''
handleBlur()
}
const queryInput = ref(null)
function handleInputFocus () {
isFocus.value = true
isFocus.value = true
}
function handleInputBlur () {
setTimeout(() => {
isFocus.value = false
}, 100)
setTimeout(() => {
isFocus.value = false
}, 100)
}
</script>
<template>
<ISearchProcess :collapse="collapse" :actions="actions" :status="processStatus" />
<template v-if="item.source && item.source.length > 0">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-link-20-solid" />
<span>来源</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<ISearchSource :source="item.source" />
</div>
</template>
<div class="text-xl flex items-center space-x-1" v-if="processStatus ==='finish'">
<UIcon name="i-heroicons-chat-bubble-left-right-20-solid" />
<span>{{ item.ansLoading ? '回答中' : '回答' }}</span>
</div>
<IMdMdc :content="item.article" />
<div class="space-x-2" v-if="item.showActions">
<UButton size="xs" color="gray" @click="handleCopyMD" leading-icon="i-heroicons-document-duplicate-20-solid" label="复制" />
<UButton size="xs" color="gray" @click="handleShare" leading-icon="i-heroicons-share-20-solid" label="分享" />
<UButton v-if="isLastIndex" size="xs" color="gray" @click="handleReGenerate" leading-icon="i-heroicons-arrow-path-rounded-square-20-solid" label="重写" />
</div>
<ISearchProcess :collapse="collapse" :actions="actions" :status="processStatus" />
<template v-if="item.source && item.source.length > 0">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-link-20-solid" />
<span>来源</span>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<ISearchSource :source="item.source" />
</div>
</template>
<div v-if="processStatus ==='finish'" class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-chat-bubble-left-right-20-solid" />
<span>{{ item.ansLoading ? '回答中' : '回答' }}</span>
</div>
<IMdMdc :content="item.article" />
<div v-if="item.showActions" class="space-x-2">
<UButton size="xs" color="gray" leading-icon="i-heroicons-document-duplicate-20-solid" label="复制" @click="handleCopyMD" />
<UButton size="xs" color="gray" leading-icon="i-heroicons-share-20-solid" label="分享" @click="handleShare" />
<UButton v-if="isLastIndex" size="xs" color="gray" leading-icon="i-heroicons-arrow-path-rounded-square-20-solid" label="重写" @click="handleReGenerate" />
</div>
</template>
<script setup>
const Search = inject('Search')
const toast = useToast()
const props = defineProps({
item: {
type: Object,
default: (() => {})
},
asking: {
type: Boolean,
default: false
},
isLastIndex: {
type: Boolean,
default: false
},
index: {
type: Number,
default: 0
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
processStatus: {
type: String,
default: '' // start | finish
}
item: {
type: Object,
default: (() => {})
},
asking: {
type: Boolean,
default: false
},
isLastIndex: {
type: Boolean,
default: false
},
index: {
type: Number,
default: 0
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
processStatus: {
type: String,
default: '' // start | finish
}
})
const emits = defineEmits(['regenerate'])
function handleReGenerate () {
emits('regenerate', props.index)
emits('regenerate', props.index)
}
function handleCopyMD () {
useCopyToClipboard().copy(props.item.article)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
useCopyToClipboard().copy(props.item.article)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
}
function handleShare () {
const hash = props.index + 1;
const url = window.location.href + (hash ? '#' + hash : '')
Search.handleUpdateOpenState(url)
function handleShare() {
const hash = props.index + 1;
const url = window.location.href + (hash ? '#' + hash : '')
Search.handleUpdateOpenState(url)
}
</script>
\ No newline at end of file
<template>
<div class="flex flex-col gap-4 sticky top-16">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-square-3-stack-3d-20-solid" />
<span>补充信息</span>
</div>
<template v-for="(item, index) in data" :key="index">
<template v-if="item.ready">
<template v-if="item.type === 'search_relate_repo'">
<template v-if="item.data.length">
<template v-for="(chart, cIndex) in item.data" :key="cIndex">
<UCard :ui="cardUI">
<template v-if="chart.info">
<div class="flex mb-2">
<div class="flex items-center gap-1 text-sm overflow-hidden">
<UIcon class="flex-shrink-0" name="i-simple-icons-github" />
<ULink class="flex-grow truncate underline" :to="chart.info.url" target="_blank" :title="chart.info.description">{{ chart.info.name }}</ULink>
<UIcon class="flex-shrink-0" name="i-heroicons-arrow-top-right-on-square" />
</div>
</div>
</template>
<ProseChart
type="line"
:labels="chart.labels"
:data="chart.data"
:info="chart.info"
simple
/>
<div class="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">{{ item.title }}</div>
</UCard>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<IEmpty size="xs"/>
</UCard>
</template>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</div>
</UCard>
</template>
</template>
</div>
<div class="flex flex-col gap-4 sticky top-16">
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-square-3-stack-3d-20-solid" />
<span>补充信息</span>
</div>
<template v-for="(item, index) in data" :key="index">
<template v-if="item.ready">
<template v-if="item.type === 'search_relate_repo'">
<template v-if="item.data.length">
<template v-for="(chart, cIndex) in item.data" :key="cIndex">
<UCard :ui="cardUI">
<template v-if="chart.info">
<div class="flex mb-2">
<div class="flex items-center gap-1 text-sm overflow-hidden">
<UIcon class="flex-shrink-0" name="i-simple-icons-github" />
<ULink class="flex-grow truncate underline" :to="chart.info.url" target="_blank" :title="chart.info.description">{{ chart.info.name }}</ULink>
<UIcon class="flex-shrink-0" name="i-heroicons-arrow-top-right-on-square" />
</div>
</div>
</template>
<ProseChart
type="line"
:labels="chart.labels"
:data="chart.data"
:info="chart.info"
simple
/>
<div class="text-xs text-center text-gray-500 dark:text-gray-400 mt-2">{{ item.title }}</div>
</UCard>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<IEmpty size="xs"/>
</UCard>
</template>
</template>
</template>
<template v-else>
<UCard :ui="cardUI">
<div class="flex flex-col gap-1">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</div>
</UCard>
</template>
</template>
</div>
</template>
<script setup>
defineProps({
data: {
type: Array,
default: () => []
}
data: {
type: Array,
default: () => []
}
})
const cardUI = {
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
}
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
}
}
</script>
<template>
<header class="sticky top-0 z-10 bg-white dark:bg-black w-full flex flex-col">
<div class="w-full p-2 justify-between flex">
<IMenuSider />
<div class="hidden md:flex">
<div class="flex items-center gap-2" v-if="repo">
<UIcon name="i-simple-icons-github" />{{ repo }}
</div>
</div>
<div class="flex-grow justify-center items-center space-x-2 hidden sm:flex">
<UTooltip text="点击修改标题" v-if="!isEditTitle">
<div @click="handleFocusTitle">{{ editTitle }}</div>
</UTooltip>
<UInput
v-else
ref="titleRef"
autofocus
:model-value="editTitle"
@blur="handleBlurTitle"
/>
</div>
<div class="flex gap-2">
<IActionThread
:collection_id="$selectCollectionId.length ? $selectCollectionId[0] : ''"
:c_id="state.id"
@delete="handleDeletedThread"
/>
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-plus-small"
:label="$selectCollectionId.length ? '已收藏' : '收藏'"
@click="handleOpenSelect"
/>
<UPopover v-model:open="isShareOpen">
<UButton
:leading-icon="isOpen ? 'i-heroicons-share-16-solid' : 'i-heroicons-lock-closed-16-solid'"
label="分享"
@click="handleSetOpenState"
/>
<template #panel>
<div class="flex flex-col p-3 gap-2 min-w-72">
<div>访问权限</div>
<div class="flex flex-col border dark:border-gray-800 rounded">
<div
class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="handleUpdateOpenState(false)"
>
<div class="flex-grow flex flex-col gap-1 text-sm">
<div class="flex items-center gap-1" :class="{ 'text-primary-500': !isOpen }">
<UIcon name="i-heroicons-lock-closed-16-solid" />
<span>私密</span>
</div>
<div class="text-xs text-gray-500">只有作者可以查看</div>
</div>
<div v-if="!isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
<UDivider />
<div
class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="handleUpdateOpenState(true)"
>
<div class="flex-grow flex flex-col gap-1 text-sm" :class="{ 'text-primary-500': isOpen }">
<div class="flex items-center gap-1">
<UIcon name="i-heroicons-share-20-solid" />
<span>可分享的</span>
</div>
<div class="text-xs text-gray-500">任何持有链接的人都可以查看</div>
</div>
<div v-if="isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
</div>
<div class="flex gap-1 items-center text-xs text-primary-500" v-if="isOpen">
<UIcon name="i-heroicons-clipboard-document-check" />
<span>链接已复制</span>
</div>
</div>
</template>
</UPopover>
</div>
</div>
<UDivider />
</header>
<header class="sticky top-0 z-10 bg-white dark:bg-black w-full flex flex-col">
<div class="w-full p-2 justify-between flex">
<IMenuSider />
<div class="hidden md:flex">
<div v-if="repo" class="flex items-center gap-2">
<UIcon name="i-simple-icons-github" />{{ repo }}
</div>
</div>
<div class="flex-grow justify-center items-center space-x-2 hidden sm:flex">
<UTooltip v-if="!isEditTitle" text="点击修改标题">
<div @click="handleFocusTitle">{{ editTitle }}</div>
</UTooltip>
<UInput
v-else
ref="titleRef"
autofocus
:model-value="editTitle"
@blur="handleBlurTitle"
/>
</div>
<div class="flex gap-2">
<IActionThread
:collection_id="$selectCollectionId.length ? $selectCollectionId[0] : ''"
:c_id="state.id"
@delete="handleDeletedThread"
/>
<UButton
color="gray"
variant="ghost"
leading-icon="i-heroicons-plus-small"
:label="$selectCollectionId.length ? '已收藏' : '收藏'"
@click="handleOpenSelect"
/>
<UPopover v-model:open="isShareOpen">
<UButton
:leading-icon="isOpen ? 'i-heroicons-share-16-solid' : 'i-heroicons-lock-closed-16-solid'"
label="分享"
@click="handleSetOpenState"
/>
<template #panel>
<div class="flex flex-col p-3 gap-2 min-w-72">
<div>访问权限</div>
<div class="flex flex-col border dark:border-gray-800 rounded">
<div
class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="handleUpdateOpenState(false)"
>
<div class="flex-grow flex flex-col gap-1 text-sm">
<div class="flex items-center gap-1" :class="{ 'text-primary-500': !isOpen }">
<UIcon name="i-heroicons-lock-closed-16-solid" />
<span>私密</span>
</div>
<div class="text-xs text-gray-500">只有作者可以查看</div>
</div>
<div v-if="!isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
<UDivider />
<div
class="flex flex-grow justify-between m-1 p-1 cursor-pointer rounded hover:bg-gray-100 dark:hover:bg-gray-800"
@click="handleUpdateOpenState(true)"
>
<div class="flex-grow flex flex-col gap-1 text-sm" :class="{ 'text-primary-500': isOpen }">
<div class="flex items-center gap-1">
<UIcon name="i-heroicons-share-20-solid" />
<span>可分享的</span>
</div>
<div class="text-xs text-gray-500">任何持有链接的人都可以查看</div>
</div>
<div v-if="isOpen">
<UIcon name="i-heroicons-check-circle-20-solid" class="text-primary-500 text-xl" />
</div>
</div>
</div>
<div v-if="isOpen" class="flex gap-1 items-center text-xs text-primary-500">
<UIcon name="i-heroicons-clipboard-document-check" />
<span>链接已复制</span>
</div>
</div>
</template>
</UPopover>
</div>
</div>
<UDivider />
</header>
</template>
<script setup>
const toast = useToast()
......@@ -94,18 +94,21 @@ const { $selectCollectionId } = storeToRefs(useLibraryStore())
const { $openLibrarySelect, $setSelectCollectionId } = useLibraryStore()
const { findRecordCollection } = useCollectionRequest()
const props = defineProps({
query: String,
isPublic: {
type: Boolean,
default: false
},
repo: {
type: String,
default: ''
}
query: {
type: String,
default: ''
},
isPublic: {
type: Boolean,
default: false
},
repo: {
type: String,
default: ''
}
})
const state = reactive({
id: route.params.id
id: route.params.id
})
const Layout = inject('Layout')
const emits = defineEmits(['update-query'])
......@@ -116,72 +119,72 @@ const isOpen = ref(false)
const isShareOpen = ref(false)
watch(()=> props.query, () => {
editTitle.value = props.query
editTitle.value = props.query
}, { immediate: true })
watch(() => props.isPublic, () => {
isOpen.value = props.isPublic
isOpen.value = props.isPublic
}, { immediate: true })
function handleFocusTitle () {
isEditTitle.value = true
isEditTitle.value = true
}
function handleBlurTitle () {
isEditTitle.value = false
emits('update-query', editTitle.value)
isEditTitle.value = false
emits('update-query', editTitle.value)
}
async function handleUpdateOpenState (s) {
const { data } = await useRequest(`/v1/chat/${state.id}/public/${s}`, { method: 'post' })
if (data.value) isOpen.value = s
const { data } = await useRequest(`/v1/chat/${state.id}/public/${s}`, { method: 'post' })
if (data.value) isOpen.value = s
}
function handleCopyLink (url) {
useCopyToClipboard().copy(url || window.location.href)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 2000,
title: '链接已复制到剪贴板'
})
useCopyToClipboard().copy(url || window.location.href)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 2000,
title: '链接已复制到剪贴板'
})
}
async function handleSetOpenState() {
if (isShareOpen.value) return
if (!isOpen.value) {
setTimeout(async () => {
await handleUpdateOpenState(true)
handleCopyLink()
}, 200)
} else {
await handleUpdateOpenState(true)
handleCopyLink()
}
if (isShareOpen.value) return
if (!isOpen.value) {
setTimeout(async () => {
await handleUpdateOpenState(true)
handleCopyLink()
}, 200)
} else {
await handleUpdateOpenState(true)
handleCopyLink()
}
}
function handleOpenSelect () {
$openLibrarySelect(state.id, $selectCollectionId.value)
$openLibrarySelect(state.id, $selectCollectionId.value)
}
defineExpose({
isOpen,
handleUpdateOpenState,
handleCopyLink
isOpen,
handleUpdateOpenState,
handleCopyLink
})
async function initData () {
const { data, error } = await findRecordCollection(state.id)
if (!error.value) {
const ids = data.value.data.map(item => item.collection_id)
$setSelectCollectionId(ids)
}
const { data, error } = await findRecordCollection(state.id)
if (!error.value) {
const ids = data.value.data.map(item => item.collection_id)
$setSelectCollectionId(ids)
}
}
// 初始化登录的时候判断是否已收藏
if ($isSignIn.value) {
await initData()
await initData()
}
function handleRemoveCollect (data) {
const { c_id } = data.value
if (c_id === state.id) {
$setSelectCollectionId([])
Layout.handleClearRemoveCollectData()
}
const { c_id } = data.value
if (c_id === state.id) {
$setSelectCollectionId([])
Layout.handleClearRemoveCollectData()
}
}
watch(()=> Layout.removeCollectData, (data) => {
if (data.value !== null) handleRemoveCollect(data)
if (data.value !== null) handleRemoveCollect(data)
}, { deep: true })
function handleDeletedThread () {
navigateTo('/')
navigateTo('/')
}
</script>
\ No newline at end of file
<template>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<ICollapse :open="openCollapse" class="mt-2">
<div v-auto-animate class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400">
<template v-if="status !== 'finish' && !actions.length">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<template v-for="(action, index) in actions" :key="action.action">
<ISearchProcessAction
:action="action"
:last="actions.length === index + 1"
:status="status"
/>
</template>
</div>
</ICollapse>
</UCard>
<UCard :ui="{ body: { padding: 'p-4 sm:p-4' } }">
<div class="flex justify-between items-center">
<div class="flex items-center gap-2 text-lg">
<UIcon name="i-heroicons-sparkles-20-solid" />
搜索过程
</div>
<UButton
size="md"
color="gray"
variant="ghost"
:icon="openCollapse ? 'i-heroicons-chevron-up-20-solid' : 'i-heroicons-chevron-down-20-solid'"
:ui="{ rounded: 'rounded-full' }"
@click="handleToggleCollapse"
/>
</div>
<ICollapse :open="openCollapse" class="mt-2">
<div v-auto-animate class="flex flex-col gap-2 w-full text-gray-500 dark:text-gray-400">
<template v-if="status !== 'finish' && !actions.length">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<template v-for="(action, index) in actions" :key="action.action">
<ISearchProcessAction
:action="action"
:last="actions.length === index + 1"
:status="status"
/>
</template>
</div>
</ICollapse>
</UCard>
</template>
<script setup>
const props = defineProps({
type: {
type: String,
default: 'search'
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
status: {
type: String,
default: ''
}
type: {
type: String,
default: 'search'
},
collapse: {
type: Boolean,
default: true
},
actions: {
type: Array,
default: () => []
},
status: {
type: String,
default: ''
}
})
const openCollapse = ref(props.collapse)
function handleToggleCollapse () {
openCollapse.value = !openCollapse.value
openCollapse.value = !openCollapse.value
}
function handleCollapse (state) {
openCollapse.value = state
openCollapse.value = state
}
watch(() => props.collapse, () => {
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 1000)
}
if (props.collapse) {
handleCollapse(true)
} else {
setTimeout(() => {
handleCollapse(false)
}, 1000)
}
}, { immediate: true})
defineExpose({ handleCollapse })
</script>
<template>
<div class="text-base flex items-center gap-1">
<UIcon :name="item.icon" />
{{ item.name }}
</div>
<template v-if="['rephrase_question', 'tool_select'].includes((action.action))">
<div class="pl-5">
<IMdMdc :content="action.output" size="sm" />
</div>
</template>
<template v-else-if="['search_file', 'search_web'].includes(action.action)">
<div class="text-xs pl-5">找到 {{ action.output.length }} 条来源</div>
</template>
<template v-if="last && status !== 'finish'">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
<div class="text-base flex items-center gap-1">
<UIcon :name="item.icon" />
{{ item.name }}
</div>
<template v-if="['rephrase_question', 'tool_select'].includes((action.action))">
<div class="pl-5">
<IMdMdc :content="action.output" size="sm" />
</div>
</template>
<template v-else-if="['search_file', 'search_web'].includes(action.action)">
<div class="text-xs pl-5">找到 {{ action.output.length }} 条来源</div>
</template>
<template v-if="last && status !== 'finish'">
<USkeleton class="h-4" />
<USkeleton class="h-4 w-2/3" />
</template>
</template>
<script setup>
const props = defineProps({
action: {
type: Object,
default: () => {}
},
last: {
type: Boolean,
default: false
},
status: {
type: String,
default: ''
}
action: {
type: Object,
default: () => {}
},
last: {
type: Boolean,
default: false
},
status: {
type: String,
default: ''
}
})
const item = computed(() => {
const action = props.action.action
let name, icon
if (action === 'rephrase_question') {
name = '理解问题'
icon = 'i-heroicons-inbox-arrow-down'
}
else if (action === 'search_file') {
name = '搜索项目'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'search_web') {
name = '搜索网页'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'tool_select') {
name = '使用工具'
icon = 'i-heroicons-puzzle-piece'
}
else if (action === 'search_relate_repo') {
name = '查找相关项目'
icon = 'i-heroicons-rectangle-group'
}
return {
name,
icon
}
const action = props.action.action
let name, icon
if (action === 'rephrase_question') {
name = '理解问题'
icon = 'i-heroicons-inbox-arrow-down'
}
else if (action === 'search_file') {
name = '搜索项目'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'search_web') {
name = '搜索网页'
icon = 'i-heroicons-magnifying-glass'
}
else if (action === 'tool_select') {
name = '使用工具'
icon = 'i-heroicons-puzzle-piece'
}
else if (action === 'search_relate_repo') {
name = '查找相关项目'
icon = 'i-heroicons-rectangle-group'
}
return {
name,
icon
}
})
</script>
\ No newline at end of file
<template>
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-rectangle-group-20-solid" />
<span>相关问题</span>
</div>
<div class="flex flex-col gap-2">
<UButton
v-for="item in recommendQuestions"
color="gray"
size="md"
variant="soft"
@click="handleClick(item.title)"
>
<div class="justify-between w-full flex items-center">
<div class="truncate">{{ item.title }}</div>
<UIcon name="i-heroicons-plus-20-solid" class="text-base" />
</div>
</UButton>
</div>
<div class="text-xl flex items-center space-x-1">
<UIcon name="i-heroicons-rectangle-group-20-solid" />
<span>相关问题</span>
</div>
<div class="flex flex-col gap-2">
<UButton
v-for="(item, index) in recommendQuestions"
:key="index"
color="gray"
size="md"
variant="soft"
@click="handleClick(item.title)"
>
<div class="justify-between w-full flex items-center">
<div class="truncate">{{ item.title }}</div>
<UIcon name="i-heroicons-plus-20-solid" class="text-base" />
</div>
</UButton>
</div>
</template>
<script setup>
defineProps({
recommendQuestions: {
type: Array,
default: () => []
}
recommendQuestions: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['click'])
function handleClick(title) {
emits('click', title)
emits('click', title)
}
</script>
\ No newline at end of file
<template>
<ULink v-for="(item, index) in limitSource" :to="item.url" :title="item.url" target="_blank">
<UCard :ui="cardUI">
<div class="flex flex-col h-full gap-1">
<template v-if="getIconPath(item.url) === 'github'">
<div class="items-center flex gap-1">
<UIcon name="i-simple-icons-github" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate">{{ item.title }}</div>
</div>
<div class="text-gray-200">{{ index + 1 }}</div>
</div>
<div class="text-blue-500 line-clamp-1">{{ item.label }}</div>
</template>
<template v-else>
<div class="flex gap-1 h-full">
<div class="line-clamp-2 text-sm">{{ item.title }}</div>
</div>
<div class="items-center flex gap-1">
<UAvatar :src="getIconPath(item.url)" size="2xs" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate text-xs text-gray-500">{{ getDomain(item.url) }}</div>
</div>
<div class="text-gray-200 text-sm">{{ index + 1 }}</div>
</div>
</template>
</div>
</UCard>
</ULink>
<UCard class="cursor-pointer" :ui="cardUI" v-if="source.length > 6" @click="handleToggleShowAll">
<div class="flex items-center justify-center h-full gap-1">
<UIcon v-if="!showAllSource" name="i-heroicons-chevron-down-20-solid" />
<UIcon v-else name="i-heroicons-chevron-up-20-solid" />
<div v-if="!showAllSource">查看全部{{ source.length }}个来源</div>
<div v-else>收起</div>
</div>
</UCard>
<ULink v-for="(item, index) in limitSource" :key="index" :to="item.url" :title="item.url" target="_blank">
<UCard :ui="cardUI">
<div class="flex flex-col h-full gap-1">
<template v-if="getIconPath(item.url) === 'github'">
<div class="items-center flex gap-1">
<UIcon name="i-simple-icons-github" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate">{{ item.title }}</div>
</div>
<div class="text-gray-200">{{ index + 1 }}</div>
</div>
<div class="text-blue-500 line-clamp-1">{{ item.label }}</div>
</template>
<template v-else>
<div class="flex gap-1 h-full">
<div class="line-clamp-2 text-sm">{{ item.title }}</div>
</div>
<div class="items-center flex gap-1">
<UAvatar :src="getIconPath(item.url)" size="2xs" class="flex-shrink-0" />
<div class="flex flex-grow overflow-hidden">
<div class="truncate text-xs text-gray-500">{{ getDomain(item.url) }}</div>
</div>
<div class="text-gray-200 text-sm">{{ index + 1 }}</div>
</div>
</template>
</div>
</UCard>
</ULink>
<UCard v-if="source.length > 6" class="cursor-pointer" :ui="cardUI" @click="handleToggleShowAll">
<div class="flex items-center justify-center h-full gap-1">
<UIcon v-if="!showAllSource" name="i-heroicons-chevron-down-20-solid" />
<UIcon v-else name="i-heroicons-chevron-up-20-solid" />
<div v-if="!showAllSource">查看全部{{ source.length }}个来源</div>
<div v-else>收起</div>
</div>
</UCard>
</template>
<script setup>
const props = defineProps({
source: {
type: Array,
default: () => []
}
source: {
type: Array,
default: () => []
}
})
const cardUI = {
base: 'h-full',
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
},
background: 'transition hover:bg-gray-100 dark:hover:bg-gray-800'
base: 'h-full',
body: {
padding: 'sm:p-2 p-2',
base: 'h-full'
},
background: 'transition hover:bg-gray-100 dark:hover:bg-gray-800'
}
const showAllSource = ref(false)
const limitSource = computed(() => {
if (props.source.length <= 6) return props.source
else if (showAllSource.value) return props.source
else return props.source.slice(0, 5)
if (props.source.length <= 6) return props.source
else if (showAllSource.value) return props.source
else return props.source.slice(0, 5)
})
function getIconPath (url) {
if (!url || !url.startsWith('http')) return ''
const uri = new URL(url)
if (uri.origin.endsWith('github.com')) return 'github'
return `https://toolb.cn/favicon/${url}`
if (!url || !url.startsWith('http')) return ''
const uri = new URL(url)
if (uri.origin.endsWith('github.com')) return 'github'
return `https://toolb.cn/favicon/${url}`
}
function getDomain(url) {
// 使用正则表达式匹配协议和域名部分
const regex = /^(https?:\/\/)?([^\/]+)/
const match = url.match(regex)
// 使用正则表达式匹配协议和域名部分
const regex = /^(https?:\/\/)?([^/]+)/
const match = url.match(regex)
// 如果匹配不到,返回空字符串
if (!match) {
return ''
}
// 如果匹配不到,返回空字符串
if (!match) {
return ''
}
// 获取域名部分
const domain = match[2]
// 获取域名部分
const domain = match[2]
// 去除可能存在的端口号
// 返回域名
return domain.split(':')[0];
// 去除可能存在的端口号
// 返回域名
return domain.split(':')[0];
}
const handleToggleShowAll = () => {
showAllSource.value = !showAllSource.value
showAllSource.value = !showAllSource.value
}
</script>
<template>
<component :is="titleTag" :class="{ 'text-3xl': titleTag !== 'div' }" :id="id">{{ title }}</component>
<component :is="titleTag" :id="id" :class="{ 'text-3xl': titleTag !== 'div' }">{{ title }}</component>
</template>
<script setup>
const props = defineProps({
as: {
type: String,
default: 'h2'
},
title: {
type: String,
default: ''
},
id: {
type: [String, Number],
default: ''
}
as: {
type: String,
default: 'h2'
},
title: {
type: String,
default: ''
},
id: {
type: [String, Number],
default: ''
}
})
const titleTag = computed(() => {
let tag = props.as
const title = props.title
if (title.indexOf('\n') > -1) tag = 'div'
else if (title.length > 50) tag = 'div'
return tag
let tag = props.as
const title = props.title
if (title.indexOf('\n') > -1) tag = 'div'
else if (title.length > 50) tag = 'div'
return tag
})
</script>
<template>
<div class="grid grid-cols-1">
<article class="prose dark:prose-invert max-w-none" v-html="mdHtml"></article>
</div>
<div class="grid grid-cols-1">
<article class="prose dark:prose-invert max-w-none" v-html="mdHtml"/>
</div>
</template>
<script setup>
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/atom-one-dark.css'
const props = defineProps({
content: String
content: {
type: String,
default: ''
}
})
const mdHtml = ref('')
const initMarkdownIt = new MarkdownIt({
......@@ -29,10 +32,10 @@ const initMarkdownIt = new MarkdownIt({
}
})
const handleRenderMd = () => {
mdHtml.value = initMarkdownIt.render(props.content || '')
mdHtml.value = initMarkdownIt.render(props.content || '')
}
watch( () => props.content, () => {
handleRenderMd()
handleRenderMd()
}, { immediate: true })
</script>
<style>
......
<template>
<div class="w-full relative" style="aspect-ratio: 2/1">
<canvas ref="refChart" :aria-label="title"></canvas>
</div>
<div class="w-full relative" style="aspect-ratio: 2/1">
<canvas ref="refChart" :aria-label="title"/>
</div>
</template>
<script setup>
import { Chart } from 'chart.js/auto';
const colorMode = useColorMode()
const props = defineProps({
type: {
type: String,
default: 'line'
},
title: {
type: String,
default: ''
},
labels: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
},
simple: {
type: Boolean,
default: false
}
type: {
type: String,
default: 'line'
},
title: {
type: String,
default: ''
},
labels: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
},
simple: {
type: Boolean,
default: false
}
})
const refChart = ref(null)
let chart
function init () {
Chart.defaults.datasets.line.fill = true
Chart.defaults.datasets.line.fill = true
if (props.simple) {
Chart.defaults.plugins.legend.display = false
}
if (props.simple) {
Chart.defaults.plugins.legend.display = false
}
chart = new Chart(refChart.value, {
type: props.type,
data: {
labels: props.labels,
datasets: props.data.map(item => {
// item.backgroundColor = ['rgba(54, 162, 235, 0.2)', 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)']
// item.borderColor = ['rgb(54, 162, 235)', 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)']
item.backgroundColor = ['rgba(54, 162, 235, 0.2)']
item.borderColor = ['rgb(54, 162, 235)']
item.borderWidth = 1
return item
})
chart = new Chart(refChart.value, {
type: props.type,
data: {
labels: props.labels,
datasets: props.data.map(item => {
// item.backgroundColor = ['rgba(54, 162, 235, 0.2)', 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)']
// item.borderColor = ['rgb(54, 162, 235)', 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)']
item.backgroundColor = ['rgba(54, 162, 235, 0.2)']
item.borderColor = ['rgb(54, 162, 235)']
item.borderWidth = 1
return item
})
},
options: {
maintainAspectRatio: false,
plugins: {
title: {
display: !props.simple,
text: props.title,
font: {
size: 14,
weight: 'normal'
}
}
},
scales: {
x: {
display: !props.simple,
grid: {
display: false
}
},
options: {
maintainAspectRatio: false,
plugins: {
title: {
display: !props.simple,
text: props.title,
font: {
size: 14,
weight: 'normal'
}
}
},
scales: {
x: {
display: !props.simple,
grid: {
display: false
}
},
y: {
display: !props.simple,
grid: {
display: false
},
stacked: true
}
},
radius: 0,
interaction: {
intersect: false,
}
y: {
display: !props.simple,
grid: {
display: false
},
stacked: true
}
})
},
radius: 0,
interaction: {
intersect: false,
}
}
})
}
function destroy () {
if (chart) {
chart.destroy()
chart = null
}
if (chart) {
chart.destroy()
chart = null
}
}
onMounted(() => {
nextTick(() => {
init()
})
nextTick(() => {
init()
})
})
onBeforeUnmount(() => {
destroy()
destroy()
})
watch(()=> colorMode.value, (value) => {
let color = '#e5e7eb'
if (value === 'dark') color = '#1f2937'
Chart.defaults.borderColor = color
if (chart) {
destroy()
init()
}
let color = '#e5e7eb'
if (value === 'dark') color = '#1f2937'
Chart.defaults.borderColor = color
if (chart) {
destroy()
init()
}
}, { immediate: true, deep: true })
</script>
<template>
<UTable
:rows="data"
:columns="columns"
:ui="config"
:sort-button="sortButton"
>
<template #repo_name-data="{ row }">
<ULink :to="'https://github.com/' + row.repo_name" target="_blank">{{ row.repo_name }}</ULink>
</template>
<template #repo_url-data="{ row }">
<ULink :to="row.repo_url" target="_blank">{{ row.repo_url }}</ULink>
</template>
</UTable>
<UTable
:rows="data"
:columns="columns"
:ui="config"
:sort-button="sortButton"
>
<template #repo_name-data="{ row }">
<ULink :to="'https://github.com/' + row.repo_name" target="_blank">{{ row.repo_name }}</ULink>
</template>
<template #repo_url-data="{ row }">
<ULink :to="row.repo_url" target="_blank">{{ row.repo_url }}</ULink>
</template>
</UTable>
</template>
<script setup>
const props = defineProps({
data: {
type: Array,
default: () => []
}
data: {
type: Array,
default: () => []
}
})
const columns = computed(() => {
const columns = []
for (const key in props.data[0]) {
columns.push({
key,
label: key,
sortable: true
})
}
const columns = []
for (const key in props.data[0]) {
columns.push({
key,
label: key,
sortable: true
})
}
return columns
return columns
})
const config = {
base: 'table-auto',
td: {
base: 'max-w-96 whitespace-normal'
}
base: 'table-auto',
td: {
base: 'max-w-96 whitespace-normal'
}
}
const sortButton = {
icon: 'i-heroicons-chevron-up-down-20-solid'
icon: 'i-heroicons-chevron-up-down-20-solid'
}
</script>
\ No newline at end of file
<template>
<NuxtLink
:href="href"
:target="target"
>
<slot />
</NuxtLink>
<NuxtLink
:href="href"
:target="target"
>
<slot />
</NuxtLink>
</template>
<script setup lang="ts">
import type { PropType } from 'vue'
defineProps({
href: {
type: String,
default: ''
},
target: {
type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined>,
default: '_blank',
required: false
}
href: {
type: String,
default: ''
},
target: {
type: String as PropType<'_blank' | '_parent' | '_self' | '_top' | (string & object) | null | undefined>,
default: '_blank',
required: false
}
})
</script>
\ No newline at end of file
<template>
<UCard :ui="cardUI">
<pre class="flex justify-between items-center m-0 p-1 pl-4 pr-1 rounded-none dark">
<UCard :ui="cardUI">
<pre class="flex justify-between items-center m-0 p-1 pl-4 pr-1 rounded-none dark">
<div>{{ language }}</div>
<UButton
leading-icon="i-heroicons-document-duplicate-20-solid"
......@@ -9,9 +9,9 @@
@click="handleCopy"
/>
</pre>
<UDivider :ui="{ border: { base: 'border-gray-700' } }" />
<pre :class="$props.class" class="m-0 rounded-none"><code v-html="codeBlock"></code></pre>
</UCard>
<UDivider :ui="{ border: { base: 'border-gray-700' } }" />
<pre :class="$props.class" class="m-0 rounded-none"><code v-html="codeBlock"/></pre>
</UCard>
</template>
<script setup>
......@@ -19,54 +19,54 @@ import hljs from 'highlight.js'
import 'highlight.js/styles/stackoverflow-dark.css'
const toast = useToast()
const props = defineProps({
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array,
default: () => []
},
meta: {
type: String,
default: null
},
class: {
type: String,
default: null
}
code: {
type: String,
default: ''
},
language: {
type: String,
default: null
},
filename: {
type: String,
default: null
},
highlights: {
type: Array,
default: () => []
},
meta: {
type: String,
default: null
},
class: {
type: String,
default: null
}
})
const cardUI = {
body: {
padding: 'p-0 sm:p-0'
},
base: 'overflow-hidden mt-5 mb-5',
ring: 'ring-0 dark:ring-1'
body: {
padding: 'p-0 sm:p-0'
},
base: 'overflow-hidden mt-5 mb-5',
ring: 'ring-0 dark:ring-1'
}
const handleCopy = () => {
useCopyToClipboard().copy(props.code)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
useCopyToClipboard().copy(props.code)
toast.add({
icon: 'i-heroicons-information-circle-20-solid',
timeout: 1000,
title: '复制成功'
})
}
const codeBlock = ref(null)
const handleRender = () => {
const language = props.language || 'html'
const lang = language.startsWith('vue') ? 'html' : language
codeBlock.value = hljs.highlight(props.code, { language: lang }).value
const language = props.language || 'html'
const lang = language.startsWith('vue') ? 'html' : language
codeBlock.value = hljs.highlight(props.code, { language: lang }).value
}
watch(()=> props.code, () => {
handleRender();
handleRender();
}, { immediate: true })
</script>
......
export default () => {
const { $getCollection } = useLibraryStore()
// 创建及修改收藏夹
const setOrUpdateCollection = async (body) => {
/*
const { $getCollection } = useLibraryStore()
// 创建及修改收藏夹
const setOrUpdateCollection = async (body) => {
/*
* id number 非必须 有ID参数是修改,没有ID则为新增
* name string 非必须
* is_public number 非必须
* description string
*
*/
const {data, error} = await useRequest('/v1/collection/merge', {
method: 'post',
body
})
return { data, error }
}
// 删除收藏夹
const deleteCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/remove`, {
method: 'post'
})
return { data, error }
}
// 将会话添加到收藏夹
const saveCollection = async (body) => {
// collection_id number 收藏夹ID
// c_id string 会话ID
const {data, error} = await useRequest(`/v1/collection/item/add`, {
method: 'post',
body
})
return { data, error }
}
// 查询收藏夹会话列表
const findCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/items`)
if (error.value) {
return []
}
return data.value.data || []
}
// 删除收藏夹会话
const deleteCollectionRecord = async (collection_id, c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/delete`, {
method: 'post',
body: { collection_id, c_id }
})
$getCollection()
return { data, error }
}
// 查询会话是否被收藏
const findRecordCollection = async (c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/check/${c_id}`)
return { data, error }
const {data, error} = await useRequest('/v1/collection/merge', {
method: 'post',
body
})
return { data, error }
}
// 删除收藏夹
const deleteCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/remove`, {
method: 'post'
})
return { data, error }
}
// 将会话添加到收藏夹
const saveCollection = async (body) => {
// collection_id number 收藏夹ID
// c_id string 会话ID
const {data, error} = await useRequest(`/v1/collection/item/add`, {
method: 'post',
body
})
return { data, error }
}
// 查询收藏夹会话列表
const findCollection = async (collection_id) => {
const {data, error} = await useRequest(`/v1/collection/${collection_id}/items`)
if (error.value) {
return []
}
return data.value.data || []
}
// 删除收藏夹会话
const deleteCollectionRecord = async (collection_id, c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/delete`, {
method: 'post',
body: { collection_id, c_id }
})
$getCollection()
return { data, error }
}
// 查询会话是否被收藏
const findRecordCollection = async (c_id) => {
const {data, error} = await useRequest(`/v1/collection/item/check/${c_id}`)
return { data, error }
}
const deleteThread = async (ids) => {
const {data, error} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
return { data, error }
}
const getRepoStars = async (params) => {
/*
const deleteThread = async (ids) => {
const {data, error} = await useRequest('/v1/chat/completion/remove', {
method: 'post',
body: ids
})
return { data, error }
}
const getRepoStars = async (params) => {
/*
url githuburl 多个使用逗号分割
per 数据统计粒度,day,week,month,默认是用week
day_period 查询周期天,默认365
*/
const { data, error } = await useRequest('/v1/repo/stars', { params })
if (error.value) return []
return data.value.data
}
return {
setOrUpdateCollection,
deleteCollection,
saveCollection,
findCollection,
deleteCollectionRecord,
findRecordCollection,
deleteThread,
getRepoStars
}
const { data, error } = await useRequest('/v1/repo/stars', { params })
if (error.value) return []
return data.value.data
}
return {
setOrUpdateCollection,
deleteCollection,
saveCollection,
findCollection,
deleteCollectionRecord,
findRecordCollection,
deleteThread,
getRepoStars
}
}
\ No newline at end of file
export default () => {
// 查询主题列表
const getThreadsList = async (c_ids) => {
// c_ids => c_id,c_id
let query = c_ids ? `?c_ids=${c_ids}` : ''
const { data, error } = await useRequest(`/v1/chat/completion/list${query}`)
if (error.value) {
return []
}
return data.value.data || []
}
return {
getThreadsList
// 查询主题列表
const getThreadsList = async (c_ids) => {
// c_ids => c_id,c_id
let query = c_ids ? `?c_ids=${c_ids}` : ''
const { data, error } = await useRequest(`/v1/chat/completion/list${query}`)
if (error.value) {
return []
}
return data.value.data || []
}
return {
getThreadsList
}
}
\ No newline at end of file
const BASE_URL = 'https://gpu-pod656e861afe3d944d6b3ce77e-7862.node.inscode.run'
const request = async (url, options = {}) => {
const token = useCookie('token')
const fullUrl = BASE_URL + url
const config = {
method: options.method || 'get',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
onRequest({ request, options }) {
// 设置请求头
},
onRequestError({ request, options, error }) {
// 处理请求错误
error && console.error(error)
},
onResponse({ request, response, options }) {
},
onResponseError({ request, response, options }) {
const status = response.status
useRequestError(status, response._data.message)
// 处理响应错误
console.log('[ResponseError]', request)
}
const token = useCookie('token')
const fullUrl = BASE_URL + url
const config = {
method: options.method || 'get',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
onRequest() {
// 设置请求头
},
onRequestError({ error }) {
// 处理请求错误
error && console.error(error)
},
onResponse() {
},
onResponseError({ request, response }) {
const status = response.status
useRequestError(status, response._data.message)
// 处理响应错误
console.log('[ResponseError]', request)
}
if (options && options.headers) {
Object.assign(config.headers, options.headers)
delete options.headers
}
return useFetch(fullUrl, Object.assign(config, options));
}
if (options && options.headers) {
Object.assign(config.headers, options.headers)
delete options.headers
}
return useFetch(fullUrl, Object.assign(config, options));
}
export default request
export default function (status, message) {
if (process.client && [400, 401, 403].includes(status)) {
// 全局弹提示
let title
if (status === 400) title = message
else if (status === 401) title = '抱歉,您尚未登录'
else if (status === 403) title = '抱歉,您没有权限'
nextTick(() => {
const toast = useToast()
toast.add({
icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 3000,
title: title,
color: 'red'
})
})
// 全局弹登录
const { $isSignIn } = storeToRefs(useUserStore())
const { $openSign } = useUserStore()
if (status === 401 && !$isSignIn.value) {
$openSign()
}
if (import.meta.client && [400, 401, 403].includes(status)) {
// 全局弹提示
let title
if (status === 400) title = message
else if (status === 401) title = '抱歉,您尚未登录'
else if (status === 403) title = '抱歉,您没有权限'
nextTick(() => {
const toast = useToast()
toast.add({
icon: 'i-heroicons-exclamation-triangle-20-solid',
timeout: 3000,
title: title,
color: 'red'
})
})
// 全局弹登录
const { $isSignIn } = storeToRefs(useUserStore())
const { $openSign } = useUserStore()
if (status === 401 && !$isSignIn.value) {
$openSign()
}
}
}
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册